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 + 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 65daedcf38..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 @@ -19,4 +20,13 @@ 3.0.0-pre.49 + + + + 9.0.0 + + + 8.10.0 + + diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.JsonWebTokens/InternalAPI.Unshipped.txt index 3242b527e7..3c1f13320b 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 Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string header, string payload, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(string jwtEncodedString, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void Microsoft.IdentityModel.JsonWebTokens.JsonWebToken.JsonWebToken(System.ReadOnlyMemory encodedTokenMemory, Microsoft.IdentityModel.Tokens.ReadTokenPayloadValueDelegate readTokenPayloadValueDelegate) -> void 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 1f38022f33..b5e6bf0eba 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -3,11 +3,13 @@ using System; using System.Net.Http; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Telemetry; namespace Microsoft.IdentityModel.Protocols { @@ -18,8 +20,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); @@ -35,6 +45,17 @@ public class ConfigurationManager : BaseConfigurationManager, IConfigurationM private const int ConfigurationRetrieverRunning = 1; 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 + // 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 +168,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; @@ -172,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, @@ -183,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( @@ -214,7 +246,24 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) { if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + 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); + } } } @@ -227,7 +276,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)); } @@ -240,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( @@ -247,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); @@ -267,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( @@ -285,15 +347,34 @@ private void UpdateCurrentConfiguration() private void UpdateConfiguration(T configuration) { _currentConfiguration = configuration; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, 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)); } /// /// 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) { @@ -309,16 +390,14 @@ public override async Task GetBaseConfigurationAsync(Cancella /// public override void RequestRefresh() { - DateTimeOffset now = DateTimeOffset.UtcNow; + DateTime now = _timeProvider.GetUtcNow().UtcDateTime; - if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) + if (now >= DateTimeUtil.Add(LastRequestRefresh, RefreshInterval) || _isFirstRefreshRequest) { _isFirstRefreshRequest = false; - if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverRunning, ConfigurationRetrieverIdle) == ConfigurationRetrieverIdle) - { - _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); - _lastRequestRefresh = now; - } + AtomicUpdateSyncAfter(now); + AtomicUpdateLastRequestRefresh(now); + _refreshRequested = true; } } 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/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/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); diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index 3e5486ba41..864be35a34 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 @@ -38,9 +80,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 @@ -64,15 +122,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/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/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/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 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/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index 83d7f5d69c..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; @@ -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 { /// @@ -39,7 +37,7 @@ public async Task GetPublicMetadata(ConfigurationManagerTheoryData( theoryData.MetadataAddress, - theoryData.ConfigurationRetreiver, + theoryData.ConfigurationRetriever, theoryData.DocumentRetriever, theoryData.ConfigurationValidator); @@ -62,7 +60,7 @@ public static TheoryData("AccountsGoogleCom") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AccountsGoogle @@ -70,7 +68,7 @@ public static TheoryData("AADCommonUrl") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrl @@ -78,7 +76,7 @@ public static TheoryData("AADCommonUrlV1") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV1 @@ -86,7 +84,7 @@ public static TheoryData("AADCommonUrlV2") { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), MetadataAddress = OpenIdConfigData.AADCommonUrlV2 @@ -101,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) @@ -120,7 +118,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -131,7 +129,7 @@ public static TheoryData { - ConfigurationRetreiver = null, + ConfigurationRetriever = null, ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -141,7 +139,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = new OpenIdConnectConfigurationValidator(), DocumentRetriever = null, ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -151,7 +149,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = null, DocumentRetriever = new HttpDocumentRetriever(), ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), @@ -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(); - 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); - 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() { @@ -276,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 @@ -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) + DateTime syncAfter = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); + if (syncAfter != DateTime.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 = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); + + if (!IdentityComparer.AreDatesEqualWithEpsilon(requestTime, syncAfter, 1)) + context.AddDiff($"ConfigurationManager._syncAfter: '{syncAfter}' should equal be within 1 second of '{requestTime}'."); IdentityComparer.AreEqual(firstFetchMetadataFailure, secondFetchMetadataFailure, context); } @@ -396,10 +355,10 @@ public async Task AutomaticRefreshInterval(ConfigurationManagerTheoryData("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); @@ -621,25 +580,34 @@ public async Task CheckSyncAfter() var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); // force a refresh by setting internal field - TestUtilities.SetField(configManager, "_syncAfter", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_syncAfter", DateTime.UtcNow.Subtract(TimeSpan.FromHours(1))); configuration = await configManager.GetConfigurationAsync(CancellationToken.None); // wait 1000ms here because update of config is run as a new task. Thread.Sleep(1000); // check that _syncAfter is greater than DateTimeOffset.UtcNow + AutomaticRefreshInterval - DateTimeOffset syncAfter = (DateTimeOffset)TestUtilities.GetField(configManager, "_syncAfter"); + DateTime syncAfter = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); if (syncAfter < minimumRefreshInterval) context.Diffs.Add($"(AutomaticRefreshInterval) syncAfter '{syncAfter}' < DateTimeOffset.UtcNow + configManager.AutomaticRefreshInterval: '{minimumRefreshInterval}'."); // make same check for RequestRefresh // force a refresh by setting internal field - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTime.UtcNow.Subtract(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"); + syncAfter = (DateTime)TestUtilities.GetField(configManager, "_syncAfter"); if (syncAfter < minimumRefreshInterval) context.Diffs.Add($"(RequestRefresh) syncAfter '{syncAfter}' < DateTimeOffset.UtcNow + configManager.AutomaticRefreshInterval: '{minimumRefreshInterval}'."); @@ -657,15 +625,14 @@ public async Task GetConfigurationAsync() configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); var configuration = await configManager.GetConfigurationAsync(CancellationToken.None); - TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); + 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); 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); @@ -751,13 +718,121 @@ 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) { 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 }); @@ -797,7 +872,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator, DocumentRetriever = new FileDocumentRetriever(), First = true, @@ -807,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)), @@ -817,7 +892,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -828,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)), @@ -838,7 +913,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -849,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)), @@ -859,7 +934,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -870,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)), @@ -880,7 +955,7 @@ public static TheoryData { - ConfigurationRetreiver = new OpenIdConnectConfigurationRetriever(), + ConfigurationRetriever = new OpenIdConnectConfigurationRetriever(), ConfigurationValidator = openIdConnectConfigurationValidator2, DocumentRetriever = new FileDocumentRetriever(), PresetCurrentConfiguration = true, @@ -926,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.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests.csproj index 1cbfb9eed3..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 @@ -11,6 +11,12 @@ true + + + true + + PreserveNewest @@ -19,6 +25,7 @@ + @@ -27,6 +34,7 @@ + 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. 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) }); 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 @@ + +