From 5853e4ccc1396a6c775aef41619b0d5798779bfc Mon Sep 17 00:00:00 2001 From: jennyf19 Date: Sun, 11 Aug 2024 18:44:04 -0700 Subject: [PATCH 1/4] try to fix codeQL (#2767) --- .github/workflows/codeql-analysis.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index a39ce0417a..85b9755976 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -1,25 +1,15 @@ name: "CodeQL" on: - push: - paths-ignore: - - 'test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/**' - - 'test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/**' - - '/src/Microsoft.IdentityModel.KeyVaultExtensions/**' - - '/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/**' - branches: [ "dev", "dev6x", "dev7x"] + push: + branches: [ "dev" ] pull_request: - paths-ignore: - - 'test/Microsoft.IdentityModel.KeyVaultExtensions.Tests/**' - - 'test/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey.Tests/**' - - '/src/Microsoft.IdentityModel.KeyVaultExtensions/**' - - '/src/Microsoft.IdentityModel.ManagedKeyVaultSecurityKey/**' types: - opened - synchronize - reopened - ready_for_review - branches: [ "dev", "dev6x", "dev7x"] + branches: [ "dev" ] jobs: analyze: From 7841666e5b9151145f8b708860f4ca3365be2840 Mon Sep 17 00:00:00 2001 From: Keegan Caruso Date: Mon, 12 Aug 2024 08:42:15 -0700 Subject: [PATCH 2/4] Fix Open Id connect parsing bug. (#2776) Error in state machine logic, missing "else". Introduced in #2657 Fixes #2772 Co-authored-by: Keegan Caruso --- .../OpenIdConnectConfigurationSerializer.cs | 4 ++-- .../End2EndTests.cs | 20 ++++++++++++++++++- ...Model.Protocols.OpenIdConnect.Tests.csproj | 2 +- .../OpenIdConfigData.cs | 2 ++ ...IdConnectMetadataEnd2EndAcrValuesLast.json | 17 ++++++++++++++++ .../OpenIdConnectTheoryData.cs | 3 +++ 6 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadataEnd2EndAcrValuesLast.json diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs index b41470dd29..57fb9d9954 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Json/OpenIdConnectConfigurationSerializer.cs @@ -163,7 +163,7 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC if (reader.ValueTextEquals(Utf8Bytes.AcrValuesSupported)) JsonPrimitives.ReadStrings(ref reader, config.AcrValuesSupported, MetadataName.AcrValuesSupported, ClassName, true); - if (reader.ValueTextEquals(Utf8Bytes.AuthorizationDetailsTypesSupported)) + else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationDetailsTypesSupported)) JsonPrimitives.ReadStrings(ref reader, config.AuthorizationDetailsTypesSupported, MetadataName.AuthorizationDetailsTypesSupported, ClassName, true); else if (reader.ValueTextEquals(Utf8Bytes.AuthorizationEndpoint)) @@ -382,7 +382,7 @@ public static OpenIdConnectConfiguration Read(ref Utf8JsonReader reader, OpenIdC if (propertyName.Equals(MetadataName.AcrValuesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.AcrValuesSupported, propertyName, ClassName); - if (propertyName.Equals(MetadataName.AuthorizationDetailsTypesSupported, StringComparison.OrdinalIgnoreCase)) + else if (propertyName.Equals(MetadataName.AuthorizationDetailsTypesSupported, StringComparison.OrdinalIgnoreCase)) JsonPrimitives.ReadStrings(ref reader, config.AuthorizationDetailsTypesSupported, propertyName, ClassName); else if (propertyName.Equals(MetadataName.AuthorizationEndpoint, StringComparison.OrdinalIgnoreCase)) diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/End2EndTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/End2EndTests.cs index 85c8282af7..d55d095692 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/End2EndTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/End2EndTests.cs @@ -22,6 +22,9 @@ public void OpenIdConnect(OpenIdConnectTheoryData theoryData) try { OpenIdConnectConfiguration configuration = OpenIdConnectConfigurationRetriever.GetAsync(theoryData.OpenIdConnectMetadataFileName, new FileDocumentRetriever(), CancellationToken.None).Result; + + theoryData.AdditionalValidation?.Invoke(configuration); + JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = tokenHandler.CreateJwtSecurityToken( @@ -102,7 +105,22 @@ public static TheoryData OpenIdConnectTheoryData() ), ExpectedException = ExpectedException.SecurityTokenSignatureKeyNotFoundException(), TestId = "Ecdsa384KeyNotPartOfJWKS" - } + }, + new OpenIdConnectTheoryData + { + OpenIdConnectMetadataFileName = OpenIdConfigData.OpenIdConnectMetadataEnd2EndAcrValuesLast, + SigningCredentials = new SigningCredentials( + KeyingMaterial.RsaSecurityKey_2048, + SecurityAlgorithms.RsaSha256 + ), + TestId = "AcrValuesLast", + AdditionalValidation = (OpenIdConnectConfiguration config) => + { + Assert.Contains("0", config.AcrValuesSupported); + Assert.Contains("id-simple", config.AcrValuesSupported); + Assert.Contains("id-multifactor", config.AcrValuesSupported); + } + }, }; } } 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 586b157dc8..860f439ff0 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 @@ -12,7 +12,7 @@ - + PreserveNewest diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs index 8f7af243a7..27e80e390e 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs @@ -170,6 +170,8 @@ public static OpenIdConnectConfiguration FullyPopulatedWithKeys public static string OpenIdConnectMetadataBadFormatString = @"{""issuer""::""https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/""}"; public static string OpenIdConnectMetadataPingLabsJWKSString = @"{""jwks_uri"": ""PingLabsJWKS.json""}"; public static string OpenIdConnectMetatadataBadJson = @"{..."; + + public static string OpenIdConnectMetadataEnd2EndAcrValuesLast = "OpenIdConnectMetadataEnd2EndAcrValuesLast.json"; #endregion #region WellKnownConfigurationStrings diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadataEnd2EndAcrValuesLast.json b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadataEnd2EndAcrValuesLast.json new file mode 100644 index 0000000000..fe6656e3c4 --- /dev/null +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectMetadataEnd2EndAcrValuesLast.json @@ -0,0 +1,17 @@ +{ + "issuer": "https://sts.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/", + "authorization_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/authorize", + "token_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/token", + "token_endpoint_auth_methods_supported": [ "client_secret_post", "private_key_jwt" ], + "jwks_uri": "JsonWebKeySetEnd2End.json", + "response_types_supported": [ "code", "id_token", "code id_token" ], + "response_modes_supported": [ "query", "fragment", "form_post" ], + "subject_types_supported": [ "pairwise" ], + "scopes_supported": [ "openid" ], + "id_token_signing_alg_values_supported": [ "RS256" ], + "microsoft_multi_refresh_token": true, + "check_session_iframe": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/checksession", + "end_session_endpoint": "https://login.windows.net/d062b2b0-9aca-4ff7-b32a-ba47231a4002/oauth2/logout", + "authorization_details_types_supported": [ "account_information" ], + "acr_values_supported": [ "0", "id-simple", "id-multifactor" ] +} diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectTheoryData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectTheoryData.cs index 891993dc83..d6fee140f6 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectTheoryData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectTheoryData.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +using System; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens; @@ -22,5 +23,7 @@ public OpenIdConnectTheoryData(string testId) : base(testId) { } public string OpenIdConnectMetadataFileName { get; set; } public SigningCredentials SigningCredentials { get; set; } + + public Action AdditionalValidation { get; set; } } } From 68ff8dfd4302dc0bf260a4c0f23363ca060f5117 Mon Sep 17 00:00:00 2001 From: Ignacio Inglese Date: Tue, 13 Aug 2024 10:08:56 +0100 Subject: [PATCH 3/4] ValidateTokenAsync: New code path (#2771) * Initial implementation of ValidateTokenAsync, ValidateJWSAsync, ValidateJWEAsync * Added InternalTokenValidationResult to hold the intermediate validation results. Moved result types to their own folder. * Removed unused delegate from TokenValidationParameters * Updated ExceptionDetail uses and failing test * Added methods to JsonWebTokenHandler to create ClaimsIdentity from ValidationParameters * Expose exception within TokenValidationResult --- .../JsonWebTokenHandler.ClaimsIdentity.cs | 145 ++++++++ ...nWebTokenHandler.ValidateToken.Internal.cs | 309 ++++++++++++++++++ .../JsonWebTokenHandler.ValidateToken.cs | 2 +- .../Delegates.cs | 8 + .../TokenHandler.cs | 19 ++ .../TokenUtilities.cs | 54 ++- .../TokenValidationParameters.cs | 7 +- .../AlgorithmValidationResult.cs | 0 .../{ => Results}/AudienceValidationResult.cs | 0 .../{ => Results/Details}/ExceptionDetail.cs | 0 .../{ => Results/Details}/LogDetail.cs | 0 .../{ => Results/Details}/MessageDetail.cs | 0 .../Results/InternalTokenValidationResult.cs | 146 +++++++++ .../{ => Results}/IssuerValidationResult.cs | 0 .../{ => Results}/LifetimeValidationResult.cs | 0 .../{ => Results}/ReplayValidationResult.cs | 0 .../SignatureValidationResult.cs | 0 .../SigningKeyValidationResult.cs | 0 .../{ => Results}/TokenDecryptionResult.cs | 0 .../{ => Results}/TokenReadingResult.cs | 0 .../TokenTypeValidationResult.cs | 0 .../{ => Results}/TokenValidationResult.cs | 79 ++++- .../{ => Results}/ValidationResult.cs | 0 ...tionParameters.IssuerValidationDelegate.cs | 16 - .../Validation/ValidationParameters.cs | 2 +- .../Validation/Validators.Issuer.cs | 6 +- .../TokenValidationParametersTests.cs | 4 +- .../SigningKeyValidationResultTests.cs | 1 - 28 files changed, 755 insertions(+), 43 deletions(-) create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ClaimsIdentity.cs create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/AlgorithmValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/AudienceValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results/Details}/ExceptionDetail.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results/Details}/LogDetail.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results/Details}/MessageDetail.cs (100%) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Results/InternalTokenValidationResult.cs rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/IssuerValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/LifetimeValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/ReplayValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/SignatureValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/SigningKeyValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/TokenDecryptionResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/TokenReadingResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/TokenTypeValidationResult.cs (100%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/TokenValidationResult.cs (73%) rename src/Microsoft.IdentityModel.Tokens/Validation/{ => Results}/ValidationResult.cs (100%) delete mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ClaimsIdentity.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ClaimsIdentity.cs new file mode 100644 index 0000000000..85d512fe2d --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ClaimsIdentity.cs @@ -0,0 +1,145 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Security.Claims; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ +#nullable enable + public partial class JsonWebTokenHandler + { + /// + /// Creates a from a . + /// + /// The to use as a source. + /// The to be used for validating the token. + /// A containing the . + internal virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken? jwtToken, ValidationParameters validationParameters) + { + // TODO: Make protected once ValidationParameters is public. + _ = jwtToken ?? throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + return CreateClaimsIdentityPrivate(jwtToken, validationParameters, GetActualIssuer(jwtToken)); + } + + /// + /// Creates a from a with the specified issuer. + /// + /// The to use as a source. + /// The to be used for validating the token. + /// Specifies the issuer for the . + /// A containing the . + internal virtual ClaimsIdentity CreateClaimsIdentity(JsonWebToken? jwtToken, ValidationParameters validationParameters, string issuer) + { + // TODO: Make protected once ValidationParameters is public. + _ = jwtToken ?? throw LogHelper.LogArgumentNullException(nameof(jwtToken)); + + if (string.IsNullOrWhiteSpace(issuer)) + issuer = GetActualIssuer(jwtToken); + + if (MapInboundClaims) + return CreateClaimsIdentityWithMapping(jwtToken, validationParameters, issuer); + + return CreateClaimsIdentityPrivate(jwtToken, validationParameters, issuer); + } + + internal override ClaimsIdentity CreateClaimsIdentityInternal( + SecurityToken securityToken, + ValidationParameters validationParameters, + string issuer) + { + return CreateClaimsIdentity(securityToken as JsonWebToken, validationParameters, issuer); + } + + private ClaimsIdentity CreateClaimsIdentityWithMapping(JsonWebToken jwtToken, ValidationParameters validationParameters, string issuer) + { + _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); + foreach (Claim jwtClaim in jwtToken.Claims) + { + bool wasMapped = _inboundClaimTypeMap.TryGetValue(jwtClaim.Type, out string? type); + + string claimType = type ?? jwtClaim.Type; + + if (claimType == ClaimTypes.Actor) + { + if (identity.Actor != null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant( + LogMessages.IDX14112, + LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), + jwtClaim.Value))); + + if (CanReadToken(jwtClaim.Value)) + { + JsonWebToken? actor = ReadToken(jwtClaim.Value) as JsonWebToken; + identity.Actor = CreateClaimsIdentity(actor, validationParameters); + } + } + + if (wasMapped) + { + Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity); + if (jwtClaim.Properties.Count > 0) + { + foreach (var kv in jwtClaim.Properties) + { + claim.Properties[kv.Key] = kv.Value; + } + } + + claim.Properties[ShortClaimTypeProperty] = jwtClaim.Type; + identity.AddClaim(claim); + } + else + { + identity.AddClaim(jwtClaim); + } + } + + return identity; + } + + private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, ValidationParameters validationParameters, string issuer) + { + _ = validationParameters ?? throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + ClaimsIdentity identity = validationParameters.CreateClaimsIdentity(jwtToken, issuer); + foreach (Claim jwtClaim in jwtToken.Claims) + { + string claimType = jwtClaim.Type; + if (claimType == ClaimTypes.Actor) + { + if (identity.Actor != null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(LogMessages.IDX14112, LogHelper.MarkAsNonPII(JwtRegisteredClaimNames.Actort), jwtClaim.Value))); + + if (CanReadToken(jwtClaim.Value)) + { + JsonWebToken? actor = ReadToken(jwtClaim.Value) as JsonWebToken; + identity.Actor = CreateClaimsIdentity(actor, validationParameters, issuer); + } + } + + if (jwtClaim.Properties.Count == 0) + { + identity.AddClaim(new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity)); + } + else + { + Claim claim = new Claim(claimType, jwtClaim.Value, jwtClaim.ValueType, issuer, issuer, identity); + + foreach (var kv in jwtClaim.Properties) + claim.Properties[kv.Key] = kv.Value; + + identity.AddClaim(claim); + } + } + + return identity; + } + } +#nullable restore +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs new file mode 100644 index 0000000000..675425fc7d --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs @@ -0,0 +1,309 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Logging; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ + public partial class JsonWebTokenHandler : TokenHandler + { + /// + /// Validates a token. + /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. + /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. + /// + /// The token to be validated. + /// The to be used for validating the token. + /// A that contains useful information for logging. + /// A that can be used to request cancellation of the asynchronous operation. + /// A . + /// + /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. + /// + /// Returned if is null or empty. + /// Returned if is null. + /// Returned if 'token.Length' is greater than . + /// Returned if is not a valid , + /// Returned if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , + internal async Task ValidateTokenAsync( + string token, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken? cancellationToken) + { + // These exceptions will be removed once we add ExceptionDetails to TokenValidationResult. + if (string.IsNullOrEmpty(token)) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; + + if (validationParameters is null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + if (token.Length > MaximumTokenSizeInBytes) + return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; + + TokenReadingResult tokenReadingResult = ReadToken(token, callContext); + if (tokenReadingResult.IsValid) + return await ValidateTokenAsync( + tokenReadingResult.SecurityToken(), + validationParameters, + callContext, + cancellationToken) + .ConfigureAwait(false); + + return new TokenValidationResult + { + Exception = tokenReadingResult.Exception, + IsValid = false + }; + } + + /// + internal async Task ValidateTokenAsync( + SecurityToken token, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken? cancellationToken) + { + // These exceptions will be removed once we add ExceptionDetails to TokenValidationResult. + if (token is null) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (validationParameters is null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + if (token is not JsonWebToken jwt) + return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; + + return await InternalValidateTokenAsync( + jwt, + validationParameters, + callContext, + cancellationToken) + .ConfigureAwait(false); + } + + /// + /// Internal method for token validation, responsible for: + /// (1) Obtaining a configuration from the . + /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. + /// + /// The JWT token. + /// The to be used for validating the token. + /// A that contains useful information for logging. + /// A that can be used to request cancellation of the asynchronous operation. + /// + private async ValueTask InternalValidateTokenAsync( + JsonWebToken jsonWebToken, + ValidationParameters validationParameters, + CallContext callContext, + CancellationToken? cancellationToken) + { + BaseConfiguration currentConfiguration = + await GetCurrentConfigurationAsync(validationParameters) + .ConfigureAwait(false); + + InternalTokenValidationResult result = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false); + + if (validationParameters.ConfigurationManager is null) + return result.ToTokenValidationResult(); + + if (result.IsValid) + { + // Set current configuration as LKG if it exists. + if (currentConfiguration is not null) + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + + return result.ToTokenValidationResult(); + } + + if (TokenUtilities.IsRecoverableExceptionType(result.ExceptionDetail.Type)) + { + // If we were still unable to validate, attempt to refresh the configuration and validate using it + // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on + // retrieval error as this case should have already been hit before. This refresh handles the case + // where a new valid configuration was somehow published during validation time. + if (currentConfiguration is not null) + { + validationParameters.ConfigurationManager.RequestRefresh(); + validationParameters.RefreshBeforeValidation = true; + BaseConfiguration lastConfig = currentConfiguration; + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. + if (lastConfig != currentConfiguration) + { + result = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false); + + if (result.IsValid) + { + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + return result.ToTokenValidationResult(); + } + } + } + + if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) + { + validationParameters.RefreshBeforeValidation = false; + validationParameters.ValidateWithLKG = true; + ExceptionDetail.ExceptionType recoverableExceptionType = result.ExceptionDetail.Type; + + BaseConfiguration[] validConfigurations = validationParameters.ConfigurationManager.GetValidLkgConfigurations(); + for (int i = 0; i < validConfigurations.Length; i++) + { + BaseConfiguration lkgConfiguration = validConfigurations[i]; + if (TokenUtilities.IsRecoverableConfigurationAndExceptionType( + jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableExceptionType)) + { + result = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration, callContext, cancellationToken).ConfigureAwait(false); + + if (result.IsValid) + return result.ToTokenValidationResult(); + } + } + } + } + + return result.ToTokenValidationResult(); + } + + private async ValueTask ValidateJWEAsync( + JsonWebToken jwtToken, + ValidationParameters validationParameters, + BaseConfiguration configuration, + CallContext callContext, + CancellationToken? cancellationToken) + { + InternalTokenValidationResult internalResult = new InternalTokenValidationResult(jwtToken, this); + + TokenDecryptionResult decryptionResult = DecryptToken(jwtToken, validationParameters, configuration, callContext); + if (!internalResult.AddResult(decryptionResult)) + return internalResult; + + TokenReadingResult readingResult = ReadToken(decryptionResult.DecryptedToken(), callContext); + if (!internalResult.AddResult(readingResult)) + return internalResult; + + JsonWebToken decryptedToken = readingResult.SecurityToken() as JsonWebToken; + + InternalTokenValidationResult jwsResult = + await ValidateJWSAsync(decryptedToken, validationParameters, configuration, callContext, cancellationToken) + .ConfigureAwait(false); + + if (!internalResult.Merge(jwsResult)) + return internalResult; + + jwtToken.InnerToken = internalResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (internalResult.SecurityToken as JsonWebToken).Payload; + + return internalResult; + } + + private async ValueTask ValidateJWSAsync( + JsonWebToken jsonWebToken, + ValidationParameters validationParameters, + BaseConfiguration configuration, + CallContext callContext, + CancellationToken? cancellationToken) + { + if (validationParameters.TransformBeforeSignatureValidation is not null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + InternalTokenValidationResult internalResult = new InternalTokenValidationResult(jsonWebToken, this); + + DateTime? expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? jsonWebToken.ValidTo : null; + DateTime? notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? jsonWebToken.ValidFrom : null; + + if (!internalResult.AddResult(validationParameters.LifetimeValidator( + notBefore, expires, jsonWebToken, validationParameters, callContext))) + return internalResult; + + if (jsonWebToken.Audiences is not IList tokenAudiences) + tokenAudiences = jsonWebToken.Audiences.ToList(); + + if (!internalResult.AddResult(validationParameters.AudienceValidator( + tokenAudiences, jsonWebToken, validationParameters, callContext))) + return internalResult; + + if (!internalResult.AddResult(await validationParameters.IssuerValidatorAsync( + jsonWebToken.Issuer, jsonWebToken, validationParameters, callContext, cancellationToken) + .ConfigureAwait(false))) + return internalResult; + + if (!internalResult.AddResult(validationParameters.TokenReplayValidator( + expires, jsonWebToken.EncodedToken, validationParameters, callContext))) + return internalResult; + + // actor validation + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + TokenReadingResult actorReadingResult = ReadToken(jsonWebToken.Actor, callContext); + if (!internalResult.AddResult(actorReadingResult)) + return internalResult; + + JsonWebToken actorToken = actorReadingResult.SecurityToken() as JsonWebToken; + ValidationParameters actorParameters = validationParameters.ActorValidationParameters; + InternalTokenValidationResult actorValidationResult = + await ValidateJWSAsync(actorToken, actorParameters, configuration, callContext, cancellationToken) + .ConfigureAwait(false); + + // Consider adding a new ValidationResult type for actor validation + // that wraps the actorValidationResult.ValidationResults + if (!internalResult.AddResults(actorValidationResult.ValidationResults)) + return internalResult; + } + + if (!internalResult.AddResult(validationParameters.TypeValidator( + jsonWebToken.Typ, jsonWebToken, validationParameters, callContext))) + return internalResult; + + // The signature validation delegate is yet to be migrated to ValidationParameters. + if (!internalResult.AddResult(ValidateSignature( + jsonWebToken, validationParameters, configuration, callContext))) + return internalResult; + + if (!internalResult.AddResult(validationParameters.IssuerSigningKeyValidator( + jsonWebToken.SigningKey, jsonWebToken, validationParameters, configuration, callContext))) + return internalResult; + + return internalResult; + } + + private static async Task GetCurrentConfigurationAsync(ValidationParameters validationParameters) + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager is not null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is tracked and dismissed as the ValidationParameters may have the issuer + // and signing key set directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + return currentConfiguration; + } + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 61b35f0544..bd576925d8 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -581,7 +581,7 @@ internal async ValueTask ValidateTokenPayloadAsync( } string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); - return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer, null) { IsValid = true, TokenType = tokenType diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index b9de79bff8..0587164690 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -206,5 +206,13 @@ namespace Microsoft.IdentityModel.Tokens /// This method is not expected to throw. /// The validated . internal delegate SignatureValidationResult SignatureValidatorDelegate(SecurityToken token, ValidationParameters validationParameters, BaseConfiguration? configuration, CallContext? callContext); + + /// + /// Transforms the security token before signature validation. + /// + /// The being validated. + /// The to be used for validating the token. + /// The transformed . + internal delegate SecurityToken TransformBeforeSignatureValidationDelegate(SecurityToken token, ValidationParameters validationParameters); #nullable restore } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenHandler.cs b/src/Microsoft.IdentityModel.Tokens/TokenHandler.cs index c9fc108317..ffb8a97232 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenHandler.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenHandler.cs @@ -124,6 +124,25 @@ internal virtual ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken secur MarkAsNonPII("internal virtual ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, TokenValidationParameters tokenValidationParameters, string issuer)"), MarkAsNonPII(GetType().FullName)))); } + + /// + /// Called by base class to create a . + /// Currently only used by the JsonWebTokenHandler when called with ValidationParameters to allow for a Lazy creation. + /// + /// the that has the Claims. + /// the that was used to validate the token. + /// the 'issuer' to use by default when creating a Claim. + /// A . + /// + internal virtual ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, ValidationParameters validationParameters, string issuer) + { + throw LogExceptionMessage( + new NotImplementedException( + FormatInvariant( + LogMessages.IDX10267, + MarkAsNonPII("internal virtual ClaimsIdentity CreateClaimsIdentityInternal(SecurityToken securityToken, ValidationParameters validationParameters, string issuer)"), + MarkAsNonPII(GetType().FullName)))); + } #endregion } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs index 2817ddd5d6..00eccab114 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenUtilities.cs @@ -250,9 +250,19 @@ internal static IEnumerable MergeClaims(IEnumerable claims, IEnume /// true if the exception is certain types of exceptions otherwise, false. internal static bool IsRecoverableException(Exception exception) { - return exception is SecurityTokenInvalidSignatureException - || exception is SecurityTokenInvalidIssuerException - || exception is SecurityTokenSignatureKeyNotFoundException; + return IsRecoverableExceptionType(ExceptionTypeForException(exception)); + } + + /// + /// Check whether the given exception type is recoverable by LKG. + /// + /// The exception type to check. + /// true if the exception is certain types of exceptions otherwise, false. + internal static bool IsRecoverableExceptionType(ExceptionDetail.ExceptionType exceptionType) + { + return exceptionType == ExceptionDetail.ExceptionType.SecurityTokenInvalidSignature + || exceptionType == ExceptionDetail.ExceptionType.SecurityTokenInvalidIssuer + || exceptionType == ExceptionDetail.ExceptionType.SecurityTokenSignatureKeyNotFound; } /// @@ -263,19 +273,35 @@ internal static bool IsRecoverableException(Exception exception) /// The LKG exception to check. /// The exception to check. /// true if the configuration is recoverable otherwise, false. - internal static bool IsRecoverableConfiguration(string kid, BaseConfiguration currentConfiguration, BaseConfiguration lkgConfiguration, Exception currentException) + internal static bool IsRecoverableConfiguration( + string kid, BaseConfiguration currentConfiguration, BaseConfiguration lkgConfiguration, Exception currentException) { - Lazy isRecoverableSigningKey = new Lazy(() => lkgConfiguration.SigningKeys.Any(signingKey => signingKey.KeyId == kid)); + return IsRecoverableConfigurationAndExceptionType( + kid, currentConfiguration, lkgConfiguration, ExceptionTypeForException(currentException)); + } - if (currentException is SecurityTokenInvalidIssuerException) + /// + /// Check whether the given configuration is recoverable by LKG. + /// + /// The kid from token."/> + /// The to check. + /// The last known good configuration to check. + /// The exception type to check. + /// true if the configuration is recoverable otherwise, false. + internal static bool IsRecoverableConfigurationAndExceptionType( + string kid, BaseConfiguration currentConfiguration, BaseConfiguration lkgConfiguration, ExceptionDetail.ExceptionType currentExceptionType) + { + Lazy isRecoverableSigningKey = new(() => lkgConfiguration.SigningKeys.Any(signingKey => signingKey.KeyId == kid)); + + if (currentExceptionType == ExceptionDetail.ExceptionType.SecurityTokenInvalidIssuer) { return currentConfiguration.Issuer != lkgConfiguration.Issuer; } - else if (currentException is SecurityTokenSignatureKeyNotFoundException) + else if (currentExceptionType == ExceptionDetail.ExceptionType.SecurityTokenSignatureKeyNotFound) { return isRecoverableSigningKey.Value; } - else if (currentException is SecurityTokenInvalidSignatureException) + else if (currentExceptionType == ExceptionDetail.ExceptionType.SecurityTokenInvalidSignature) { SecurityKey currentSigningKey = currentConfiguration.SigningKeys.FirstOrDefault(x => x.KeyId == kid); if (currentSigningKey == null) @@ -287,5 +313,17 @@ internal static bool IsRecoverableConfiguration(string kid, BaseConfiguration cu return false; } + + static ExceptionDetail.ExceptionType ExceptionTypeForException(Exception exception) + { + if (exception is SecurityTokenInvalidSignatureException) + return ExceptionDetail.ExceptionType.SecurityTokenInvalidSignature; + else if (exception is SecurityTokenInvalidIssuerException) + return ExceptionDetail.ExceptionType.SecurityTokenInvalidIssuer; + else if (exception is SecurityTokenSignatureKeyNotFoundException) + return ExceptionDetail.ExceptionType.SecurityTokenSignatureKeyNotFound; + else + return ExceptionDetail.ExceptionType.Unknown; + } } } diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index c7f2857186..ab93e8f58a 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -66,7 +66,6 @@ protected TokenValidationParameters(TokenValidationParameters other) IssuerSigningKeyValidatorUsingConfiguration = other.IssuerSigningKeyValidatorUsingConfiguration; IssuerValidator = other.IssuerValidator; IssuerValidatorAsync = other.IssuerValidatorAsync; - IssuerValidationDelegateAsync = other.IssuerValidationDelegateAsync; IssuerValidatorUsingConfiguration = other.IssuerValidatorUsingConfiguration; LifetimeValidator = other.LifetimeValidator; LogTokenId = other.LogTokenId; @@ -240,7 +239,11 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, if (LogHelper.IsEnabled(EventLogLevel.Informational)) LogHelper.LogInformation(LogMessages.IDX10245, securityToken); - return ClaimsIdentityFactory.Create(authenticationType: AuthenticationType ?? DefaultAuthenticationType, nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType, securityToken); + return ClaimsIdentityFactory.Create( + authenticationType: AuthenticationType ?? DefaultAuthenticationType, + nameType: nameClaimType ?? ClaimsIdentity.DefaultNameClaimType, + roleType: roleClaimType ?? ClaimsIdentity.DefaultRoleClaimType, + securityToken); } /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AlgorithmValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/AlgorithmValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/AlgorithmValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/AlgorithmValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/AudienceValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/AudienceValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/AudienceValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ExceptionDetail.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ExceptionDetail.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/LogDetail.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/LogDetail.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/MessageDetail.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/MessageDetail.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/InternalTokenValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/InternalTokenValidationResult.cs new file mode 100644 index 0000000000..c4efc371c5 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/InternalTokenValidationResult.cs @@ -0,0 +1,146 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; + +namespace Microsoft.IdentityModel.Tokens +{ +#nullable enable + /// + /// Internal class used to track the results of token validation and to provide a way to merge results. + /// Once all validation is complete, the results can be converted to a TokenValidationResult. + /// + internal class InternalTokenValidationResult + { + private bool _isValid; + private SecurityToken? _securityToken; + private TokenHandler _tokenHandler; + private List _validationResults = []; + + /// + /// Creates a new instance of to aggregate validation results. + /// + /// The being validated. + /// The performing the validation. + /// + public InternalTokenValidationResult(SecurityToken? securityToken, TokenHandler tokenHandler) + { + _securityToken = securityToken; + _tokenHandler = tokenHandler ?? throw new ArgumentNullException(nameof(tokenHandler)); + _isValid = true; + } + + /// + /// Adds a to the aggregated list of validation results. + /// + /// The to store. + /// The current IsValid value for the validation. + /// + public bool AddResult(ValidationResult validationResult) + { + if (validationResult == null) + throw new ArgumentNullException(nameof(validationResult)); + + _validationResults.Add(validationResult); + _isValid = _isValid && validationResult.IsValid; + + return IsValid; + } + + /// + /// Adds a list of to the aggregated list of validation results. + /// + /// The list of to store. + /// The current IsValid value for the validation. + /// + public bool AddResults(IList validationResults) + { + if (validationResults == null) + throw new ArgumentNullException(nameof(validationResults)); + + foreach (var validationResult in validationResults) + { + _ = AddResult(validationResult); + } + + return IsValid; + } + + /// + /// Gets the for the first failed validation result. + /// + public ExceptionDetail? ExceptionDetail + { + get + { + if (ValidationResults.Count == 0) + return null; + + // Iterate in reverse since the failure should be the last result + for (int i = ValidationResults.Count - 1; i >= 0; i--) + { + ValidationResult validationResult = ValidationResults[i]; + if (validationResult.ExceptionDetail != null) + return validationResult.ExceptionDetail; + } + + return null; + } + } + + /// + /// Gets a value indicating whether the token is valid. + /// + public bool IsValid => _isValid; + + /// + /// Merges the results of another into this instance. + /// Updates the and in case they changed. + /// + /// The to be merged. + /// + public bool Merge(InternalTokenValidationResult other) + { + _securityToken = other._securityToken; + _tokenHandler = other._tokenHandler; + + return AddResults(other.ValidationResults); + } + + /// + /// Gets the being validated. + /// + public SecurityToken? SecurityToken => _securityToken; + + /// + /// Returns a based on the aggregated validation results. + /// + /// The containing the result of aggregating all the individual results. + public TokenValidationResult ToTokenValidationResult() + { + if (IsValid) + { + // TokenValidationResult uses TokenValidationParameters to create ClaimsIdentity. + // We need to figure the best way to refactor that, ideally without creating a new TokenValidationResult class. + return new TokenValidationResult( + _securityToken, _tokenHandler, new TokenValidationParameters(), "issuer", _validationResults) + { + IsValid = true + }; + } + + return new TokenValidationResult + { + IsValid = false, + Exception = ExceptionDetail?.GetException(), // Need to introduce ExceptionDetail to TokenValidationResult + }; + } + + /// + /// Gets the list of that were aggregated. + /// + public IList ValidationResults => _validationResults; + } +#nullable restore +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/IssuerValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/IssuerValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/LifetimeValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/LifetimeValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/LifetimeValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ReplayValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/ReplayValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/ReplayValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/ReplayValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/SignatureValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/SignatureValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/SignatureValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/SigningKeyValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/SigningKeyValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/SigningKeyValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenDecryptionResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenDecryptionResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/TokenDecryptionResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenDecryptionResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenReadingResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenReadingResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/TokenReadingResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenReadingResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenTypeValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenTypeValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenValidationResult.cs similarity index 73% rename from src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenValidationResult.cs index 46c90738e0..9e435baea5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/TokenValidationResult.cs @@ -16,7 +16,8 @@ namespace Microsoft.IdentityModel.Tokens /// public class TokenValidationResult { - private readonly TokenValidationParameters _validationParameters; + private readonly TokenValidationParameters _tokenValidationParameters; + private readonly ValidationParameters _validationParameters; private readonly TokenHandler _tokenHandler; // Fields lazily initialized in a thread-safe manner. _claimsIdentity is protected by the _claimsIdentitySyncObj @@ -34,7 +35,7 @@ public class TokenValidationResult private Dictionary _claims; private Dictionary _propertyBag; // TODO - lazy creation of _validationResults - private List _validationResults = []; + private List _validationResults; private Exception _exception; private bool _isValid; @@ -47,16 +48,47 @@ public TokenValidationResult() } /// - /// This ctor is used by the JsonWebTokenHandler as part of delaying creation of ClaimsIdentity. + /// Initializes a new instance of using . /// - /// + /// The + /// + /// + /// + /// + /// This constructor is used by JsonWebTokenHandler as part of delaying creation of ClaimsIdentity. + internal TokenValidationResult( + SecurityToken securityToken, + TokenHandler tokenHandler, + TokenValidationParameters tokenValidationParameters, + string issuer, + List validationResults) + { + _tokenValidationParameters = tokenValidationParameters; + _tokenHandler = tokenHandler; + _validationResults = validationResults; + Issuer = issuer; + SecurityToken = securityToken; + } + + /// + /// Initializes a new instance of using . + /// + /// The /// /// /// - internal TokenValidationResult(SecurityToken securityToken, TokenHandler tokenHandler, TokenValidationParameters validationParameters, string issuer) + /// + /// This constructor is used by JsonWebTokenHandler as part of delaying creation of ClaimsIdentity. + internal TokenValidationResult( + SecurityToken securityToken, + TokenHandler tokenHandler, + ValidationParameters validationParameters, + string issuer, + List validationResults) { _validationParameters = validationParameters; _tokenHandler = tokenHandler; + _validationResults = validationResults; Issuer = issuer; SecurityToken = securityToken; } @@ -133,9 +165,12 @@ internal ClaimsIdentity ClaimsIdentityNoLocking { Debug.Assert(_claimsIdentity is null); - if (_validationParameters != null && SecurityToken != null && _tokenHandler != null && Issuer != null) + if (SecurityToken != null && _tokenHandler != null && Issuer != null) { - _claimsIdentity = _tokenHandler.CreateClaimsIdentityInternal(SecurityToken, _validationParameters, Issuer); + if (_tokenValidationParameters != null) + _claimsIdentity = _tokenHandler.CreateClaimsIdentityInternal(SecurityToken, _tokenValidationParameters, Issuer); + else if (_validationParameters != null) + _claimsIdentity = _tokenHandler.CreateClaimsIdentityInternal(SecurityToken, _validationParameters, Issuer); } _claimsIdentityInitialized = true; @@ -176,6 +211,12 @@ public Exception Exception get { HasValidOrExceptionWasRead = true; + if (_exception is null) + { + if (ExceptionDetail is not null) + return ExceptionDetail.GetException(); + } + return _exception; } set @@ -184,6 +225,28 @@ public Exception Exception } } + /// + /// Gets the for the first failed validation result. + /// + private ExceptionDetail ExceptionDetail + { + get + { + if (ValidationResults.Count == 0) + return null; + + // Iterate in reverse since the failure should be the last result + for (int i = ValidationResults.Count - 1; i >= 0; i--) + { + ValidationResult validationResult = ValidationResults[i]; + if (validationResult.ExceptionDetail != null) + return validationResult.ExceptionDetail; + } + + return null; + } + } + internal bool HasValidOrExceptionWasRead { get; set; } /// @@ -254,7 +317,7 @@ internal IReadOnlyList ValidationResults if (_validationResults is null) Interlocked.CompareExchange(ref _validationResults, new List(), null); - return _validationResults; + return _validationResults.AsReadOnly(); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidationResult.cs similarity index 100% rename from src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidationResult.cs diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs deleted file mode 100644 index cab11a6bbc..0000000000 --- a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -namespace Microsoft.IdentityModel.Tokens -{ - /// - /// partial class for the IssuerValidation delegate. - /// - public partial class TokenValidationParameters - { - /// - /// Gets or sets a delegate that will be used to validate the issuer of a . - /// - internal IssuerValidationDelegateAsync IssuerValidationDelegateAsync { get; set; } - } -} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index 52267a769a..318f121ecb 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -324,7 +324,7 @@ public IssuerValidationDelegateAsync IssuerValidatorAsync /// /// Gets or sets a delegate that will be called to transform a token to a supported format before validation. /// - public TransformBeforeSignatureValidation TransformBeforeSignatureValidation { get; set; } + public TransformBeforeSignatureValidationDelegate TransformBeforeSignatureValidation { get; set; } /// /// Allows overriding the delegate that will be used to validate the lifetime of the token diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index 46803152ab..87fca5f32a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -24,7 +24,7 @@ internal delegate Task IssuerValidationDelegateAsync( SecurityToken securityToken, ValidationParameters validationParameters, CallContext callContext, - CancellationToken cancellationToken); + CancellationToken? cancellationToken); /// /// IssuerValidation @@ -46,7 +46,7 @@ internal static async Task ValidateIssuerAsync( SecurityToken securityToken, ValidationParameters validationParameters, CallContext callContext, - CancellationToken cancellationToken) + CancellationToken? cancellationToken) { if (string.IsNullOrWhiteSpace(issuer)) { @@ -88,7 +88,7 @@ internal static async Task ValidateIssuerAsync( BaseConfiguration configuration = null; if (validationParameters.ConfigurationManager != null) - configuration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false); + configuration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken ?? CancellationToken.None).ConfigureAwait(false); // Return failed IssuerValidationResult if all possible places to validate against are null or empty. if (validationParameters.ValidIssuers.Count == 0 && string.IsNullOrWhiteSpace(configuration?.Issuer)) diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 38ab7b0af7..f8fcadb3cb 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 60; + int ExpectedPropertyCount = 59; [Fact] public void Publics() @@ -71,7 +71,6 @@ public void Publics() IssuerSigningKey = issuerSigningKey, IssuerSigningKeyResolver = (token, securityToken, keyIdentifier, tvp) => { return new List { issuerSigningKey }; }, IssuerSigningKeys = issuerSigningKeys, - IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync, IssuerValidator = ValidationDelegates.IssuerValidatorEcho, LifetimeValidator = ValidationDelegates.LifetimeValidatorReturnsTrue, LogTokenId = true, @@ -291,7 +290,6 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.IssuerSigningKeyResolverUsingConfiguration = ValidationDelegates.IssuerSigningKeyResolverUsingConfiguration; validationParameters.IssuerSigningKeyValidator = ValidationDelegates.IssuerSigningKeyValidator; validationParameters.IssuerSigningKeyValidatorUsingConfiguration = ValidationDelegates.IssuerSigningKeyValidatorUsingConfiguration; - validationParameters.IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync; validationParameters.IssuerValidator = ValidationDelegates.IssuerValidatorEcho; validationParameters.IssuerValidatorAsync = ValidationDelegates.IssuerValidatorInternalAsync; validationParameters.IssuerValidatorUsingConfiguration = ValidationDelegates.IssuerValidatorUsingConfigEcho; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs index 8e3d9a8fd2..1a2f055d3d 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/SigningKeyValidationResultTests.cs @@ -5,7 +5,6 @@ using System.Diagnostics; using System.IdentityModel.Tokens.Jwt; using Microsoft.IdentityModel.Logging; -using Microsoft.IdentityModel.Protocols.OpenIdConnect; using Microsoft.IdentityModel.TestUtils; using Xunit; From eb4df8fc5a9a4fbf50de2dd5bfd263a225c7c87a Mon Sep 17 00:00:00 2001 From: BrentSchmaltz Date: Thu, 15 Aug 2024 09:42:00 -0700 Subject: [PATCH 4/4] Add lock when configuration is null (#2780) Co-authored-by: id4s --- .../OpenIdConnectConfigurationValidator.cs | 13 +- .../Configuration/ConfigurationManager.cs | 237 ++++++++---- .../Configuration/HttpDocumentRetriever.cs | 26 +- .../ConfigurationManagerTests.cs | 356 ++++++++++++------ .../OpenIdConfigData.cs | 215 +++++++++++ .../OpenIdConnectSerializationTests.cs | 2 + .../ExtensibilityTests.cs | 20 +- .../InMemoryDocumentRetriever.cs | 38 ++ .../SampleListener.cs | 2 +- 9 files changed, 699 insertions(+), 210 deletions(-) create mode 100644 test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs diff --git a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs index b5ab88c555..ae5b756c5c 100644 --- a/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs +++ b/src/Microsoft.IdentityModel.Protocols.OpenIdConnect/Configuration/OpenIdConnectConfigurationValidator.cs @@ -37,11 +37,20 @@ public ConfigurationValidationResult Validate(OpenIdConnectConfiguration openIdC Succeeded = false }; } - var numberOfValidKeys = openIdConnectConfiguration.JsonWebKeySet.Keys.Where(key => key.ConvertedSecurityKey != null).Count(); + + int numberOfValidKeys = 0; + for (int i = 0; i < openIdConnectConfiguration.JsonWebKeySet.Keys.Count; i++) + if (openIdConnectConfiguration.JsonWebKeySet.Keys[i].ConvertedSecurityKey != null) + numberOfValidKeys++; if (numberOfValidKeys < MinimumNumberOfKeys) { - var convertKeyInfos = string.Join("\n", openIdConnectConfiguration.JsonWebKeySet.Keys.Where(key => !string.IsNullOrEmpty(key.ConvertKeyInfo)).Select(key => key.Kid.ToString() + ": " + key.ConvertKeyInfo)); + string convertKeyInfos = string.Join( + "\n", + openIdConnectConfiguration.JsonWebKeySet.Keys.Where( + key => !string.IsNullOrEmpty(key.ConvertKeyInfo)) + .Select(key => key.Kid.ToString() + ": " + key.ConvertKeyInfo)); + return new ConfigurationValidationResult { ErrorMessage = LogHelper.FormatInvariant( diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs index c923422ab1..3ab4c16aef 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/ConfigurationManager.cs @@ -19,17 +19,23 @@ namespace Microsoft.IdentityModel.Protocols public class ConfigurationManager : BaseConfigurationManager, IConfigurationManager where T : class { private DateTimeOffset _syncAfter = DateTimeOffset.MinValue; - private DateTimeOffset _lastRefresh = DateTimeOffset.MinValue; + private DateTimeOffset _lastRequestRefresh = DateTimeOffset.MinValue; private bool _isFirstRefreshRequest = true; + private readonly SemaphoreSlim _configurationNullLock = new SemaphoreSlim(1); - private readonly SemaphoreSlim _refreshLock; private readonly IDocumentRetriever _docRetriever; private readonly IConfigurationRetriever _configRetriever; private readonly IConfigurationValidator _configValidator; private T _currentConfiguration; - private Exception _fetchMetadataFailure; private TimeSpan _bootstrapRefreshInterval = TimeSpan.FromSeconds(1); + // task states are used to ensure the call to 'update config' (UpdateCurrentConfiguration) is a singleton. Uses Interlocked.CompareExchange. + // metadata is not being obtained + private const int ConfigurationRetrieverIdle = 0; + // metadata is being retrieved + private const int ConfigurationRetrieverRunning = 1; + private int _configurationRetrieverState = ConfigurationRetrieverIdle; + /// /// Instantiates a new that manages automatic and controls refreshing on configuration data. /// @@ -91,7 +97,6 @@ public ConfigurationManager(string metadataAddress, IConfigurationRetriever c MetadataAddress = metadataAddress; _docRetriever = docRetriever; _configRetriever = configRetriever; - _refreshLock = new SemaphoreSlim(1); } /// @@ -144,83 +149,174 @@ public async Task GetConfigurationAsync() public virtual async Task GetConfigurationAsync(CancellationToken cancel) { if (_currentConfiguration != null && _syncAfter > DateTimeOffset.UtcNow) - { return _currentConfiguration; - } - await _refreshLock.WaitAsync(cancel).ConfigureAwait(false); - try + Exception fetchMetadataFailure = null; + + // LOGIC + // if configuration == null => configuration has never been retrieved. + // reach out to the metadata endpoint. Since multiple threads could be calling this method + // we need to ensure that only one thread is actually fetching the metadata. + // else + // if task is running, return the current configuration + // else kick off task to update current configuration + if (_currentConfiguration == null) { - if (_syncAfter <= DateTimeOffset.UtcNow) + await _configurationNullLock.WaitAsync(cancel).ConfigureAwait(false); + if (_currentConfiguration != null) { - 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.. - var configuration = await _configRetriever.GetConfigurationAsync(MetadataAddress, _docRetriever, CancellationToken.None).ConfigureAwait(false); - if (_configValidator != null) - { - ConfigurationValidationResult result = _configValidator.Validate(configuration); - if (!result.Succeeded) - throw LogHelper.LogExceptionMessage(new InvalidConfigurationException(LogHelper.FormatInvariant(LogMessages.IDX20810, result.ErrorMessage))); - } + _configurationNullLock.Release(); + return _currentConfiguration; + } - _lastRefresh = DateTimeOffset.UtcNow; - // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider. - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + - TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); - _currentConfiguration = configuration; - } - catch (Exception ex) + 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. + T configuration = await _configRetriever.GetConfigurationAsync( + MetadataAddress, + _docRetriever, + CancellationToken.None).ConfigureAwait(false); + + if (_configValidator != null) { - _fetchMetadataFailure = ex; + 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))); + } - if (_currentConfiguration == null) // Throw an exception if there's no configuration to return. - { - if (_bootstrapRefreshInterval < RefreshInterval) - { - // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval. - TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds)); - _bootstrapRefreshInterval += _bootstrapRefreshInterval; - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, _bootstrapRefreshIntervalWithJitter); - } - else - { - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); - } + // Add a random amount between 0 and 5% of AutomaticRefreshInterval jitter to avoid spike traffic to IdentityProvider. + _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval + + TimeSpan.FromSeconds(new Random().Next((int)AutomaticRefreshInterval.TotalSeconds / 20))); - throw LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant(LogMessages.IDX20803, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(_syncAfter), LogHelper.MarkAsNonPII(ex)), ex)); + _currentConfiguration = configuration; + } + catch (Exception ex) + { + fetchMetadataFailure = ex; + + // In this case configuration was never obtained. + if (_currentConfiguration == null) + { + if (_bootstrapRefreshInterval < RefreshInterval) + { + // Adopt exponential backoff for bootstrap refresh interval with a decorrelated jitter if it is not longer than the refresh interval. + TimeSpan _bootstrapRefreshIntervalWithJitter = TimeSpan.FromSeconds(new Random().Next((int)_bootstrapRefreshInterval.TotalSeconds)); + _bootstrapRefreshInterval += _bootstrapRefreshInterval; + _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, _bootstrapRefreshIntervalWithJitter); } else { - _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); - - LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant(LogMessages.IDX20806, LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), LogHelper.MarkAsNonPII(ex)), ex)); + _syncAfter = DateTimeUtil.Add( + DateTime.UtcNow, + AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); } + + throw LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20803, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(ex)), + ex)); } + else + { + _syncAfter = DateTimeUtil.Add( + DateTime.UtcNow, + AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); + + LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20806, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(ex)), + ex)); + } + } + finally + { + _configurationNullLock.Release(); } + } + else + { + if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning) + { + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + } + } - // Stale metadata is better than no metadata - if (_currentConfiguration != null) - return _currentConfiguration; + // If metadata exists return it. + if (_currentConfiguration != null) + return _currentConfiguration; + + throw LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20803, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + LogHelper.MarkAsNonPII(_syncAfter), + LogHelper.MarkAsNonPII(fetchMetadataFailure)), + fetchMetadataFailure)); + } + + /// + /// This should be called when the configuration needs to be updated either from RequestRefresh or AutomaticRefresh + /// The Caller should first check the state checking state using: + /// if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning). + /// + private void UpdateCurrentConfiguration() + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + T configuration = _configRetriever.GetConfigurationAsync( + MetadataAddress, + _docRetriever, + CancellationToken.None).ConfigureAwait(false).GetAwaiter().GetResult(); + + if (_configValidator == null) + { + _currentConfiguration = configuration; + } else - throw LogHelper.LogExceptionMessage( - new InvalidOperationException( - LogHelper.FormatInvariant( - LogMessages.IDX20803, - LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), - LogHelper.MarkAsNonPII(_syncAfter), - LogHelper.MarkAsNonPII(_fetchMetadataFailure)), - _fetchMetadataFailure)); + { + ConfigurationValidationResult result = _configValidator.Validate(configuration); + + if (!result.Succeeded) + LogHelper.LogExceptionMessage( + new InvalidConfigurationException( + LogHelper.FormatInvariant( + LogMessages.IDX20810, + result.ErrorMessage))); + else + _currentConfiguration = configuration; + } + } + catch (Exception ex) + { + LogHelper.LogExceptionMessage( + new InvalidOperationException( + LogHelper.FormatInvariant( + LogMessages.IDX20806, + LogHelper.MarkAsNonPII(MetadataAddress ?? "null"), + ex), + ex)); } finally { - _refreshLock.Release(); + _syncAfter = DateTimeUtil.Add(DateTime.UtcNow, AutomaticRefreshInterval < RefreshInterval ? AutomaticRefreshInterval : RefreshInterval); + Interlocked.Exchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle); } +#pragma warning restore CA1031 // Do not catch general exception types } /// @@ -231,10 +327,8 @@ public virtual async Task GetConfigurationAsync(CancellationToken cancel) /// 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) { - var obj = await GetConfigurationAsync(cancel).ConfigureAwait(false); - if (obj is BaseConfiguration) - return obj as BaseConfiguration; - return null; + T obj = await GetConfigurationAsync(cancel).ConfigureAwait(false); + return obj as BaseConfiguration; } /// @@ -245,14 +339,15 @@ public override async Task GetBaseConfigurationAsync(Cancella public override void RequestRefresh() { DateTimeOffset now = DateTimeOffset.UtcNow; - if (_isFirstRefreshRequest) + + if (now >= DateTimeUtil.Add(_lastRequestRefresh.UtcDateTime, RefreshInterval) || _isFirstRefreshRequest) { - _syncAfter = now; _isFirstRefreshRequest = false; - } - else if (now >= DateTimeUtil.Add(_lastRefresh.UtcDateTime, RefreshInterval)) - { - _syncAfter = now; + _lastRequestRefresh = now; + if (Interlocked.CompareExchange(ref _configurationRetrieverState, ConfigurationRetrieverIdle, ConfigurationRetrieverRunning) != ConfigurationRetrieverRunning) + { + _ = Task.Run(UpdateCurrentConfiguration, CancellationToken.None); + } } } diff --git a/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs b/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs index 9eb4c2291b..c69174c757 100644 --- a/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs +++ b/src/Microsoft.IdentityModel.Protocols/Configuration/HttpDocumentRetriever.cs @@ -84,17 +84,22 @@ public HttpDocumentRetriever(HttpClient httpClient) public async Task GetDocumentAsync(string address, CancellationToken cancel) { if (string.IsNullOrWhiteSpace(address)) - throw LogHelper.LogArgumentNullException("address"); + throw LogHelper.LogArgumentNullException(nameof(address)); if (!Utility.IsHttps(address) && RequireHttps) - throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX20108, address), nameof(address))); + throw LogHelper.LogExceptionMessage( + new ArgumentException( + LogHelper.FormatInvariant( + LogMessages.IDX20108, + LogHelper.MarkAsNonPII(address)), + nameof(address))); Exception unsuccessfulHttpResponseException; HttpResponseMessage response; try { if (LogHelper.IsEnabled(EventLogLevel.Verbose)) - LogHelper.LogVerbose(LogMessages.IDX20805, address); + LogHelper.LogVerbose(LogMessages.IDX20805, LogHelper.MarkAsNonPII(address)); var httpClient = _httpClient ?? _defaultHttpClient; var uri = new Uri(address, UriKind.RelativeOrAbsolute); @@ -104,13 +109,24 @@ public async Task GetDocumentAsync(string address, CancellationToken can if (response.IsSuccessStatusCode) return responseContent; - unsuccessfulHttpResponseException = new IOException(LogHelper.FormatInvariant(LogMessages.IDX20807, address, response, responseContent)); + unsuccessfulHttpResponseException = new IOException( + LogHelper.FormatInvariant( + LogMessages.IDX20807, + LogHelper.MarkAsNonPII(address), + response, + responseContent)); + unsuccessfulHttpResponseException.Data.Add(StatusCode, response.StatusCode); unsuccessfulHttpResponseException.Data.Add(ResponseContent, responseContent); } catch (Exception ex) { - throw LogHelper.LogExceptionMessage(new IOException(LogHelper.FormatInvariant(LogMessages.IDX20804, address), ex)); + throw LogHelper.LogExceptionMessage( + new IOException( + LogHelper.FormatInvariant( + LogMessages.IDX20804, + LogHelper.MarkAsNonPII(address)), + ex)); } throw LogHelper.LogExceptionMessage(unsuccessfulHttpResponseException); diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/ConfigurationManagerTests.cs index d103db30ba..3d4cb1ea7f 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.IdentityModel.Logging; using Microsoft.IdentityModel.Protocols.Configuration; using Microsoft.IdentityModel.Protocols.OpenIdConnect.Configuration; using Microsoft.IdentityModel.TestUtils; @@ -290,163 +291,245 @@ public void GetSets() TestUtilities.AssertFailIfErrors("ConfigurationManager_GetSets", context.Errors); } - [Fact] - public void GetConfiguration() + [Theory, MemberData(nameof(AutomaticIntervalTestCases), DisableDiscoveryEnumeration = true)] + public async Task AutomaticRefreshInterval(ConfigurationManagerTheoryData theoryData) { - var docRetriever = new FileDocumentRetriever(); - var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - var context = new CompareContext($"{this}.GetConfiguration"); + var context = new CompareContext($"{this}.AutomaticRefreshInterval"); - // AutomaticRefreshInterval interval should return same config. - var configuration = configManager.GetConfigurationAsync().Result; - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - var configuration2 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); + try + { - // AutomaticRefreshInterval should pick up new bits. - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configManager.RequestRefresh(); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configManager.RequestRefresh(); - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2)"); + var configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(configuration, theoryData.ExpectedConfiguration, context); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2) (2)"); + theoryData.ConfigurationManager.MetadataAddress = theoryData.UpdatedMetadataAddress; + TestUtilities.SetField(theoryData.ConfigurationManager, "_syncAfter", theoryData.SyncAfter); + var updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + // we wait 50 ms here to make the task is finished. + Thread.Sleep(50); + updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(updatedConfiguration, theoryData.ExpectedUpdatedConfiguration, context); - // RefreshInterval is set to MaxValue - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - configManager.RefreshInterval = TimeSpan.MaxValue; - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2) (3)"); + theoryData.ExpectedException.ProcessNoException(context); + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - // First force refresh should pickup new config - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2), should be different"); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2) (4)"); - // Next force refresh shouldn't pickup new config, as RefreshInterval hasn't passed - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata.json"; - var configuration3 = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration2, configuration3, context); - if (!object.ReferenceEquals(configuration2, configuration3)) - context.Diffs.Add("!object.ReferenceEquals(configuration2, configuration3) (5)"); - // Next force refresh should pickup config since, RefreshInterval is set to 1s - configManager.RefreshInterval = TimeSpan.FromSeconds(1); - Thread.Sleep(1000); - configManager.RequestRefresh(); - var configuration4 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration2, configuration4)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration2, configuration4), should be different"); - if (object.ReferenceEquals(configuration2, configuration4)) - context.Diffs.Add("object.ReferenceEquals(configuration2, configuration4) (6)"); + TestUtilities.AssertFailIfErrors(context); + } - // Refresh should force pickup of new config - configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); - configManager.RequestRefresh(); - configManager.MetadataAddress = "OpenIdConnectMetadata2.json"; - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration, configuration2)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2), should be different"); + public static TheoryData> AutomaticIntervalTestCases + { + get + { + var theoryData = new TheoryData>(); - if (object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("object.ReferenceEquals(configuration, configuration2)"); + // Failing to get metadata returns existing. + theoryData.Add(new ConfigurationManagerTheoryData("HttpFault_ReturnExisting") + { + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, + SyncAfter = DateTime.UtcNow - TimeSpan.FromDays(2), + UpdatedMetadataAddress = "https://httpstat.us/429" + }); - // Refresh set to MaxValue - configManager.RefreshInterval = TimeSpan.MaxValue; - configuration = configManager.GetConfigurationAsync().Result; - IdentityComparer.AreEqual(configuration, configuration2, context); - if (!object.ReferenceEquals(configuration, configuration2)) - context.Diffs.Add("!object.ReferenceEquals(configuration, configuration2)"); + // AutomaticRefreshInterval interval should return same config. + theoryData.Add(new ConfigurationManagerTheoryData("AutomaticRefreshIntervalNotHit") + { + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, + SyncAfter = DateTime.UtcNow + TimeSpan.FromDays(2), + UpdatedMetadataAddress = "AADCommonV2Json" + }); + + // AutomaticRefreshInterval should pick up new bits. + theoryData.Add(new ConfigurationManagerTheoryData("AutomaticRefreshIntervalHit") + { + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, + SyncAfter = DateTime.UtcNow, + UpdatedMetadataAddress = "AADCommonV2Json" + }); - // get configuration from http address, should throw - configManager = new ConfigurationManager("http://127.0.0.1", new OpenIdConnectConfigurationRetriever()); - var ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(ArgumentException)); - try + return theoryData; + } + } + + [Theory, MemberData(nameof(RequestRefreshTestCases), DisableDiscoveryEnumeration = true)] + public async Task RequestRefresh(ConfigurationManagerTheoryData theoryData) + { + var context = new CompareContext($"{this}.RequestRefresh"); + + var configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + IdentityComparer.AreEqual(configuration, theoryData.ExpectedConfiguration, context); + + // the first call to RequestRefresh will trigger a refresh with ConfigurationManager.RefreshInterval being ignored. + // Testing RefreshInterval requires a two calls, the second call will trigger a refresh with ConfigurationManager.RefreshInterval being used. + if (theoryData.RequestRefresh) { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); + theoryData.ConfigurationManager.RequestRefresh(); + configuration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); } - catch (AggregateException ex) + + theoryData.ConfigurationManager.RefreshInterval = theoryData.RefreshInterval; + theoryData.ConfigurationManager.MetadataAddress = theoryData.UpdatedMetadataAddress; + if (theoryData.SleepTimeInMs > 0) + Thread.Sleep(theoryData.SleepTimeInMs); + + theoryData.ConfigurationManager.RequestRefresh(); + + if (theoryData.SleepTimeInMs > 0) + Thread.Sleep(theoryData.SleepTimeInMs); + + var updatedConfiguration = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + IdentityComparer.AreEqual(updatedConfiguration, theoryData.ExpectedUpdatedConfiguration, context); + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData> RequestRefreshTestCases + { + get { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + var theoryData = new TheoryData>(); - ex.Handle((x) => + // RefreshInterval set to 1 sec should return new config. + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_TimeSpan_1000ms") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, + RefreshInterval = TimeSpan.FromSeconds(1), + RequestRefresh = true, + SleepTimeInMs = 1000, + UpdatedMetadataAddress = "AADCommonV2Json" }); - } - // get configuration from https address, should throw - configManager = new ConfigurationManager("https://127.0.0.1", new OpenIdConnectConfigurationRetriever()); - ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)); - try - { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); - } - catch (AggregateException ex) - { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + // RefreshInterval set to TimeSpan.MaxValue should return same config. + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_TimeSpan_MaxValue") + { + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV1Config, + RefreshInterval = TimeSpan.MaxValue, + RequestRefresh = true, + UpdatedMetadataAddress = "AADCommonV2Json" + }); - ex.Handle((x) => + // First RequestRefresh should pickup new config + theoryData.Add(new ConfigurationManagerTheoryData("RequestRefresh_FirstRefresh") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "AADCommonV1Json", + new OpenIdConnectConfigurationRetriever(), + InMemoryDocumentRetriever), + ExpectedConfiguration = OpenIdConfigData.AADCommonV1Config, + ExpectedUpdatedConfiguration = OpenIdConfigData.AADCommonV2Config, + SleepTimeInMs = 100, + UpdatedMetadataAddress = "AADCommonV2Json" }); + + return theoryData; } + } + + [Theory, MemberData(nameof(HttpFailuresTestCases), DisableDiscoveryEnumeration = true)] + public async Task HttpFailures(ConfigurationManagerTheoryData theoryData) + { + var context = new CompareContext($"{this}.HttpFailures"); - // get configuration with unsuccessful HTTP response status code - configManager = new ConfigurationManager("https://httpstat.us/429", new OpenIdConnectConfigurationRetriever()); - ee = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)); try { - configuration = configManager.GetConfigurationAsync().Result; - ee.ProcessNoException(context); + _ = await theoryData.ConfigurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + theoryData.ExpectedException.ProcessNoException(context); } - catch (AggregateException ex) + catch (Exception ex) { - // this should throw, because last configuration retrived was null - Assert.Throws(() => configuration = configManager.GetConfigurationAsync().Result); + theoryData.ExpectedException.ProcessException(ex, context); + } - ex.Handle((x) => + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData> HttpFailuresTestCases + { + get + { + var theoryData = new TheoryData>(); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTPS_Status_Error") { - ee.ProcessException(x, context); - return true; + ConfigurationManager = new ConfigurationManager( + "https://httpstat.us/429", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)), + }); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTPS_Error") + { + ConfigurationManager = new ConfigurationManager( + "https://127.0.0.1", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(IOException)), }); + + theoryData.Add(new ConfigurationManagerTheoryData("LocalHost_HTTP_ArgumentError") + { + ConfigurationManager = new ConfigurationManager( + "http://127.0.0.1", + new OpenIdConnectConfigurationRetriever(), + new HttpDocumentRetriever()), + ExpectedException = new ExpectedException(typeof(InvalidOperationException), "IDX20803:", typeof(ArgumentException)), + }); + + return theoryData; } + } + + [Fact] + public async Task GetConfigurationAsync() + { + var docRetriever = new FileDocumentRetriever(); + var configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); + var context = new CompareContext($"{this}.GetConfiguration"); // Unable to obtain a new configuration, but _currentConfiguration is not null so it should be returned. configManager = new ConfigurationManager("OpenIdConnectMetadata.json", new OpenIdConnectConfigurationRetriever(), docRetriever); - configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + var configuration = await configManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); configManager.RequestRefresh(); configManager.MetadataAddress = "http://127.0.0.1"; - configuration2 = configManager.GetConfigurationAsync().Result; + var configuration2 = await configManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false); 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); } @@ -545,9 +628,11 @@ public void ValidateOpenIdConnectConfigurationTests(ConfigurationManagerTheoryDa { //create a listener and enable it for logs var listener = TestUtils.SampleListener.CreateLoggerListener(EventLevel.Warning); - configuration = configurationManager.GetConfigurationAsync().Result; + // we need to sleep here to make sure the task that updates configuration has finished. + Thread.Sleep(250); + if (!string.IsNullOrEmpty(theoryData.ExpectedErrorMessage) && !listener.TraceBuffer.Contains(theoryData.ExpectedErrorMessage)) context.AddDiff($"Expected exception to contain: '{theoryData.ExpectedErrorMessage}'.{Environment.NewLine}Log is:{Environment.NewLine}'{listener.TraceBuffer}'"); @@ -674,17 +759,28 @@ public static TheoryData : TheoryDataBase + private static InMemoryDocumentRetriever InMemoryDocumentRetriever => new InMemoryDocumentRetriever( + new Dictionary + { + { "AADCommonV1Json", OpenIdConfigData.AADCommonV1Json }, + { "https://login.microsoftonline.com/common/discovery/keys", OpenIdConfigData.AADCommonV1JwksString }, + { "AADCommonV2Json", OpenIdConfigData.AADCommonV2Json }, + { "https://login.microsoftonline.com/common/discovery/v2.0/keys", OpenIdConfigData.AADCommonV2JwksString } + }); + + public class ConfigurationManagerTheoryData : TheoryDataBase where T : class { - public ConfigurationManagerTheoryData() { } + public ConfigurationManager ConfigurationManager { get; set; } - public ConfigurationManagerTheoryData(string testId) : base(testId) { } + public ConfigurationManagerTheoryData() {} + + public ConfigurationManagerTheoryData(string testId) : base(testId) {} public TimeSpan AutomaticRefreshInterval { get; set; } public IConfigurationRetriever ConfigurationRetreiver { get; set; } - public IConfigurationValidator ConfigurationValidator { get; set; } + public IConfigurationValidator ConfigurationValidator { get; set; } public IDocumentRetriever DocumentRetriever { get; set; } @@ -692,18 +788,30 @@ public ConfigurationManagerTheoryData(string testId) : base(testId) { } public string ExpectedErrorMessage { get; set; } + public T ExpectedConfiguration { get; set; } + + public T ExpectedUpdatedConfiguration { get; set; } + + public DateTimeOffset LastRefreshTime { get; set; } = DateTime.MinValue; + public string MetadataAddress { get; set; } public bool PresetCurrentConfiguration { get; set; } - public TimeSpan RefreshInterval { get; set; } + public TimeSpan RefreshInterval { get; set; } = BaseConfigurationManager.DefaultRefreshInterval; public bool RequestRefresh { get; set; } + public int SleepTimeInMs { get; set; } = 0; + + public DateTimeOffset SyncAfter { get; set; } = DateTime.UtcNow; + public override string ToString() { return $"{TestId}, {MetadataAddress}, {ExpectedException}"; } + + public string UpdatedMetadataAddress { get; set; } } } } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs index 27e80e390e..1e3606cb77 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConfigData.cs @@ -274,6 +274,92 @@ public static OpenIdConnectConfiguration AccountsGoogleComConfig } """; + // 7/22/2024 + public static string AADCommonV1JwksString => + """ + { + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "x5t": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "n": "hu2SJrLlDOUtU2s9T6-6OVGEPaba2zIT2_Jl50f4NGG-r-GyQdaOzTFASfAfMkMfMQMRnabqd-dp_Ooqha473bw6DMbM23nv2uhBn5Afp-S1W_d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO_twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb_OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJB4tJM2GkZjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNTIyMTg1ODQwWhcNMjkwNTIyMTg1ODQwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhu2SJrLlDOUtU2s9T6+6OVGEPaba2zIT2/Jl50f4NGG+r+GyQdaOzTFASfAfMkMfMQMRnabqd+dp/Ooqha473bw6DMbM23nv2uhBn5Afp+S1W/d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO/twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb/OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQIDAQABoyEwHzAdBgNVHQ4EFgQUeGPdsxkVp8lIRku0u41SCzqW7LIwDQYJKoZIhvcNAQELBQADggEBAHMJCPO473QQJtTXJ49OhZ48kVCiVgbut+xElHxvBWQrfJ4Zb6WAi2RudjwrpwchVBciwjIelp/3Ryp5rVL94D479Ta/C5BzWNm9LsZCw3rPrsIvUdx26GmfQomHyL18AJQyBj8jZ+pVvdprvbV7v586TcgY24pW018IiYGQEO/fR8DSO4eN8ekTvT8hODBoKiJ9NFy+BruqW1AbMDptH12uzpU/N9bftysnWeDJEVZd5Rj8u8F9MRbB6V7dzxdoswaKkiJbxt+JrZgdtHSFqz6rDypIkumYwUkyiwH4/GQGPiyBLFbRp1EYVa3SFwAEmhl4a7On05aHVnOfCoyj/qA=" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "x5t": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "n": "yfNcG8Ka_b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt_KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq-qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6_1FgdaR62gfayjLSJepB8M-o7yC8sOtRhatwe9kbO_5QJC54B8ni0ge5i9nANMln-9ZCHeRQYkgl0RSvR_KtfpWrEqAa4K2cyPaDqejOs8G8V0kM_8CLtDWi5diKpO_fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8-_ufq-5QFJkyNUpYB9R1mVQwmRGdY0nQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJtuCSyF4i1FMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNjA5MTkxNzM5WhcNMjkwNjA5MTkxNzM5WjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyfNcG8Ka/b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt/KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq+qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6/1FgdaR62gfayjLSJepB8M+o7yC8sOtRhatwe9kbO/5QJC54B8ni0ge5i9nANMln+9ZCHeRQYkgl0RSvR/KtfpWrEqAa4K2cyPaDqejOs8G8V0kM/8CLtDWi5diKpO/fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8+/ufq+5QFJkyNUpYB9R1mVQwmRGdY0nQIDAQABoyEwHzAdBgNVHQ4EFgQUzF0gtMcVDEn4JoNlDOxvhM8IHBswDQYJKoZIhvcNAQELBQADggEBAJe2muR0H2h3phiZ/v6FD8Yio6niulN9jr7+eC/UJV1M7l5xdHgVL83JbNZjUECDrJ/m+ICY1NbEXfv4fo3sfpU1AwG5GXAhxTrS4zMhH7Hvir3800wCd3ByJ/2vQW1y3orlqR8Q65BN9ayub6BCBTNmtUAOpAWcnP3FnGtIDmAL4APcacK92ZTg8ayVX586U7DDWmI4l7X6xCruK0ic5W2b13k2cay0EalHNWHl+gikqQg6tTGSvM295P6Xy5bQ1I5QtHjVCbm0315T/FylvR8fZhVD+AUCc1DwtOr3Yhm3EXftDb6hP08C4yDhGIDH3Q3+xuWlIA7KQjgljuiT67U=" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "x5t": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "n": "q0sct8P8TxXmXX2QXzIhMnwZHdCO96SMFCMtfswF1TpxYqaIFObIhT_zxxpBTsvkYHAxG7CUQ6qVgd_TQhMx0TSZq_X3_0NG6cIRik0g-Woe0gT6tUJ-o6zdtO-6EvoOXovT3YMh8vN1Q5UJV6dudDqjnlTNHH1OxFcU4U6no1R6iILDMci_TGq7I2AJS5i_O9Ptp5NmgDT_kbwZHJz1Abbw4VuOPMFJ2Q1rN9odV9YHKjjowqa3BULVyTvP8FoGUzhoopu6O7oA-ehlO9fhEoSS0zNn0lWXQMZXUF7GSyui12121kIXyll2KlvuETQNdVkeXu0m95g_pnX-8iZ_cw", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIINBmSj+3xdxwwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA2MTIxNjA3MjBaFw0yOTA2MTIxNjA3MjBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrSxy3w/xPFeZdfZBfMiEyfBkd0I73pIwUIy1+zAXVOnFipogU5siFP/PHGkFOy+RgcDEbsJRDqpWB39NCEzHRNJmr9ff/Q0bpwhGKTSD5ah7SBPq1Qn6jrN2077oS+g5ei9PdgyHy83VDlQlXp250OqOeVM0cfU7EVxThTqejVHqIgsMxyL9MarsjYAlLmL870+2nk2aANP+RvBkcnPUBtvDhW448wUnZDWs32h1X1gcqOOjCprcFQtXJO8/wWgZTOGiim7o7ugD56GU71+EShJLTM2fSVZdAxldQXsZLK6LXbXbWQhfKWXYqW+4RNA11WR5e7Sb3mD+mdf7yJn9zAgMBAAGjITAfMB0GA1UdDgQWBBSZbhe/r/sxfv0nYlyrwjx+b6W2RTANBgkqhkiG9w0BAQsFAAOCAQEAoWZ+C/snZySK1KiOsrn1iq7wrVzkuModPMZEshR3SuDIB6+C76fmP42I3UtDVIY5EeE79YjdwDwy86dPZjKVNbP7yUSbJC8uPM1TNMA9s8QpO6RZ63ZZ4i8hcgk6PXgi0PPjX2cmzUSNUa4gS8ibhf7JDu4aF9lUceBsNQghNQfz3tBs1ksJoW3WY5EfW6yMCv1Vim0uBlpnYdlynAd8O+9N2JR9wC+12PwPGrdGQDX3pos8bnmBxM55ueiGoqDH5yGI1h63POlGnpEdqOONT8N4cZNazQ1NswbBQuZMZSfbPXjiiFQ4bktyiXr421KbknQrkRYogi1F2Cjrd4SZJg==" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "x5t": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "n": "08xqQ-OBv9jvWmtvWw8g3IkcuDHVOAGCn3K6TXyKie0L6cAyQWNX4vqxbt0cHdaLunrzaFJ2mIGj_qfor8KR_FOFVFOF24FAakB5El96LvsTwlWJNIw4kpf1O_xibycZ_UcDAEqABJfe51JSPh-PxI2sXt0UMapSjvTdnps0Conp11Ay_yupl_h7nawVg0kzw3QDX5-vKTruiHAHr845YwDRW1yJLEgkUPYXdM8d_SrRgqb2RKJEN8D1c4-SUpFHKwGAwLgVYH1cqwADX9el857z_2uKqJoP48l8WqUOfNGdvx79RCgF1NzzRh07EQrk0GJ_EB8eO-EF4YHLPImVtQ", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIIRf5MUh/1XVIwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA3MTAxNjA0NTBaFw0yOTA3MTAxNjA0NTBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTzGpD44G/2O9aa29bDyDciRy4MdU4AYKfcrpNfIqJ7QvpwDJBY1fi+rFu3Rwd1ou6evNoUnaYgaP+p+ivwpH8U4VUU4XbgUBqQHkSX3ou+xPCVYk0jDiSl/U7/GJvJxn9RwMASoAEl97nUlI+H4/Ejaxe3RQxqlKO9N2emzQKienXUDL/K6mX+HudrBWDSTPDdANfn68pOu6IcAevzjljANFbXIksSCRQ9hd0zx39KtGCpvZEokQ3wPVzj5JSkUcrAYDAuBVgfVyrAANf16XznvP/a4qomg/jyXxapQ580Z2/Hv1EKAXU3PNGHTsRCuTQYn8QHx474QXhgcs8iZW1AgMBAAGjITAfMB0GA1UdDgQWBBQGoURL0sKGdYALEdvfObZ6NEgmJTANBgkqhkiG9w0BAQsFAAOCAQEAl/UkmIa4OvsgULkBmGIZ6HeyJDvVuORphBK9/vpxEFsgnlitwMncBO54uMjJVr63baV490ODSI+ZTiCh7WGM+zrSjllCbVWDxjrdXA1ygHnXX7bXecIQyDmVb5/Hfb7DmQ4MHa3lEwf+pNS5XJeOhPoduRsfYCdD0QbxEADDgqV4FtgYx4I+iAoqbPDPou7wchEu9d3MuFuTMorkTvDLCyTHi2rgBnk9GBf2rArCGyTpvVPGXmxBttqgm9krFRujLj00u9jKUx4YkmAhS9YRddME8+gh6X4qFMxQMhyzkaBxjLs/E+pwMJaUwBqvostwt9+52qrMSUo+jkFgiGCe4Q==" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "x5t": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "n": "w1kH9dFGdaJS8fvQulDssuuNhkczzy1Mo6IiNoC3ih3K-L_VF5TQmSkqXrovWCUlhBCfc1VPR9Cn2G4UP7Sygn0nTqXBY1NFQQZecqwGESJFIuonRqjdlDhNYXjSF_eg63KyuyLV8A-Sn05Ufuc8ax0tyrxPbkOql0pB2hmRhj94iDAFB2LBoxfEgxCG3VT0ascVYW6voTCChs2P65-4RLC-ib1w1FjuACDwsB7KZDxxaUGLfnIoLWUjmw1zCaDRiRvhxB4jQXpB64IFxaYsqxA_x8bj2JEE7qALZ2dZ3fPy9yYSAnRfaTMetgouR9x4SKy4HxUxsADMm_7p9LiRZQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAO8yTjZIibNNMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA1MDYyMzA5MDJaFw0yOTA1MDYyMzA5MDJaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMNZB/XRRnWiUvH70LpQ7LLrjYZHM88tTKOiIjaAt4odyvi/1ReU0JkpKl66L1glJYQQn3NVT0fQp9huFD+0soJ9J06lwWNTRUEGXnKsBhEiRSLqJ0ao3ZQ4TWF40hf3oOtysrsi1fAPkp9OVH7nPGsdLcq8T25DqpdKQdoZkYY/eIgwBQdiwaMXxIMQht1U9GrHFWFur6EwgobNj+ufuESwvom9cNRY7gAg8LAeymQ8cWlBi35yKC1lI5sNcwmg0Ykb4cQeI0F6QeuCBcWmLKsQP8fG49iRBO6gC2dnWd3z8vcmEgJ0X2kzHrYKLkfceEisuB8VMbAAzJv+6fS4kWUCAwEAAaMhMB8wHQYDVR0OBBYEFJ4xtCt3JpPxlUVH7ATgJGM4ofg7MA0GCSqGSIb3DQEBCwUAA4IBAQB9WAEvE3VtO5wIOtN5N/QbIU63H5QPgMW3M9nOs43AhLgwvWupxaiATyMqtK53RPvcxYPe7QwSw/xH9McXii1bOBVmc71AcjlXYfuMJ/0IMEFEUQwZDEwj+vIlg07gWh0hleehyAgMblDUQRRN+b5J+soa9LBBAooY/48F/++y4DiTzKyoWn5cV4H2kdIFVyB43XzJRqDoK1ZhplVLTc1a3K1NL1/qP9rhvtx62YDzfNh4+FTJLu31ALcUbD+Qx2m0U9wuWq3EdUzEen5DeLvhx55YD7V1BASHNYBd8lGhHk97aTw53CMGAuTELvWO+4x7dFM9autw2KvSn76n/4Ql" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FB8_wii85nv_UW3qrldTvWwg-rE", + "x5t": "FB8_wii85nv_UW3qrldTvWwg-rE", + "n": "vusbA5UBNtCB0U2RmyQOCE-8fWl8bzCQXm3V5Nd7oockcyCpqXOWfhVNJD-Ifb5_zAmxRgHvRdfpA2btaqZiit5XaFYngtRK6mVxCcnOEgwxQGX9DLM5plXWtGTf_DF1FATBidFlM8KgicTS3MTyKZNrnTz0JD7ISxwV0TgSEiRrsm7eVsumuNYNW30Yb38DRDTei9U1YR0YDmdZyuf-OKTllxKH_BO-aj8Gkxcnkdriih2CINF6M6oASOHTJYO7P8CQE1DX2y2Zq7xxVvzm4IClk7WDdzuAoC-ZiKvDaU5plSyrnH3_VgjJrzXtuGN-HEd4Vg89h_2rE74cN5KRtQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAJOO92n+BJBLMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA2MTAxODA3NDVaFw0yOTA2MTAxODA3NDVaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7rGwOVATbQgdFNkZskDghPvH1pfG8wkF5t1eTXe6KHJHMgqalzln4VTSQ/iH2+f8wJsUYB70XX6QNm7WqmYoreV2hWJ4LUSuplcQnJzhIMMUBl/QyzOaZV1rRk3/wxdRQEwYnRZTPCoInE0tzE8imTa5089CQ+yEscFdE4EhIka7Ju3lbLprjWDVt9GG9/A0Q03ovVNWEdGA5nWcrn/jik5ZcSh/wTvmo/BpMXJ5Ha4oodgiDRejOqAEjh0yWDuz/AkBNQ19stmau8cVb85uCApZO1g3c7gKAvmYirw2lOaZUsq5x9/1YIya817bhjfhxHeFYPPYf9qxO+HDeSkbUCAwEAAaMhMB8wHQYDVR0OBBYEFLBW6P0A+qHESOFg8Rgxqp38myYtMA0GCSqGSIb3DQEBCwUAA4IBAQAsZzkzk8w7RR3KCHOY+XLn3R2NanL/j+WILdOHnJn9Ot1VbG868MFQgwMp8Y2y7Kj5RekknY6EGcNuJi4rLgq5u1LSB/IoNPCs7l3MhRQqoedJX4sDNf4NfTVHK+4GNSQqP60eBoxClRexIbKcHJ0x57Ww/S9NNWtldBIfB7egoSj6UVcTHRLWZyPoZsOXHY4bYOf8ANNg21jT1KWwOXSWUx60v7tVxEXs8XAEUnmuMbuh3yAnjv3UoRdl7wcaQ5jq2/+vaAWZm0WlWN3CCY3y2mE0OZZg9HRCQu+o+58wt658sDIpP7PXGjyA5h23W9+i8QtyQ1PtqCXKj8zktivW" + ] + }, + { + "kty": "RSA", + "use": "sig", + "kid": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "x5t": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "n": "iJd0N795eVyYQvWe417HOF_GHlRgOsPZRh1KwNHyWP_WKrjlOl8ftPAs-Sspv-s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk_Fa4K7sutgQdyxKehhRnk4hAIc-mUM9ROrkyr4dIi-Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB-uDX2Z_hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB-wBEe-SF5bKVNSfExcsiL0KzV_iQkKQNALJrakX4Mw-hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F-y1h873iOkYWiebNIu5TeTjVww", + "e": "AQAB", + "x5c": [ + "MIIC6TCCAdGgAwIBAgIIHxgoSKq7mWgwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI0MDYxMDE4MjgyOFoXDTI5MDYxMDE4MjgyOFowIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiJd0N795eVyYQvWe417HOF/GHlRgOsPZRh1KwNHyWP/WKrjlOl8ftPAs+Sspv+s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk/Fa4K7sutgQdyxKehhRnk4hAIc+mUM9ROrkyr4dIi+Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB+uDX2Z/hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB+wBEe+SF5bKVNSfExcsiL0KzV/iQkKQNALJrakX4Mw+hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F+y1h873iOkYWiebNIu5TeTjVwwIDAQABoyEwHzAdBgNVHQ4EFgQUcLvbIYVCbexuF1KXcKysM8kS6EMwDQYJKoZIhvcNAQELBQADggEBAF95Wf/yAfmHksmL42JiCemjsHN0KlZ2NsGTj2+zbDXbttj8zm+ZA74bPlAWI5aFvKfxxpC3Chfi26+GhKVeVRA65KyokTulQzE+BWbqphQZoH6Iz07J3GB3uUthPQbedtj6SDD/zE4jcmhmrY8o0lU5zJhkp9T5f8644ZR6rJRIXpFbDwmbsFM5H4Nz7D5FG+A4uYumICoTaiQjJ+cu/k8sDM8ut6R2cGmwlRMIGzD8HzNeGuaRtXsFqCGAI+qRbW29hJoFNZxhQBeFRDdBvwbNIa/o6ZAzKq81E4SdV1d33oM3vWDMBlR3b46a1d+Unm1Ou8uJ2yDfqMrZ7/NGNV8=" + ] + } + ] + } + """; + public static OpenIdConnectConfiguration AADCommonV1Config { get @@ -306,6 +392,7 @@ public static OpenIdConnectConfiguration AADCommonV1Config config.AdditionalData.Add("cloud_graph_host_name", "graph.windows.net"); config.AdditionalData.Add("msgraph_host", "graph.microsoft.com"); config.AdditionalData.Add("rbac_url", "https://pas.windows.net"); + config.JsonWebKeySet = JsonWebKeySet.Create(AADCommonV1JwksString); return config; } @@ -342,6 +429,133 @@ public static OpenIdConnectConfiguration AADCommonV1Config } """; + public static string AADCommonV2JwksString => + """ + { + "keys": [ + { + "kty": "RSA", + "use": "sig", + "kid": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "x5t": "23ntaxfH9Gk-8D_USzoAJgwjyt8", + "n": "hu2SJrLlDOUtU2s9T6-6OVGEPaba2zIT2_Jl50f4NGG-r-GyQdaOzTFASfAfMkMfMQMRnabqd-dp_Ooqha473bw6DMbM23nv2uhBn5Afp-S1W_d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO_twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb_OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJB4tJM2GkZjMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNTIyMTg1ODQwWhcNMjkwNTIyMTg1ODQwWjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhu2SJrLlDOUtU2s9T6+6OVGEPaba2zIT2/Jl50f4NGG+r+GyQdaOzTFASfAfMkMfMQMRnabqd+dp/Ooqha473bw6DMbM23nv2uhBn5Afp+S1W/d4NxEhfNlN1Tgjx3Sh6UblBSFCE4JGkugSkLi2SVouy43seskesQotXGVNv4iboFm4yO/twlMCG9EDwza32y6WZtV8i9gkQP42OfK0X1qy6EUz2DN7cpfZtmkNtsFJhFf9waOvNCR95LVCPGafeCOMAQEvu1VO3mrBSIg7Izu0CzvuaBQTwnGv29Ggxc3GO4gvb/OStkkmfIwchu3A8F6e0aJ4Ys8PFP7z7Z8lqQIDAQABoyEwHzAdBgNVHQ4EFgQUeGPdsxkVp8lIRku0u41SCzqW7LIwDQYJKoZIhvcNAQELBQADggEBAHMJCPO473QQJtTXJ49OhZ48kVCiVgbut+xElHxvBWQrfJ4Zb6WAi2RudjwrpwchVBciwjIelp/3Ryp5rVL94D479Ta/C5BzWNm9LsZCw3rPrsIvUdx26GmfQomHyL18AJQyBj8jZ+pVvdprvbV7v586TcgY24pW018IiYGQEO/fR8DSO4eN8ekTvT8hODBoKiJ9NFy+BruqW1AbMDptH12uzpU/N9bftysnWeDJEVZd5Rj8u8F9MRbB6V7dzxdoswaKkiJbxt+JrZgdtHSFqz6rDypIkumYwUkyiwH4/GQGPiyBLFbRp1EYVa3SFwAEmhl4a7On05aHVnOfCoyj/qA=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "x5t": "MGLqj98VNLoXaFfpJCBpgB4JaKs", + "n": "yfNcG8Ka_b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt_KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq-qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6_1FgdaR62gfayjLSJepB8M-o7yC8sOtRhatwe9kbO_5QJC54B8ni0ge5i9nANMln-9ZCHeRQYkgl0RSvR_KtfpWrEqAa4K2cyPaDqejOs8G8V0kM_8CLtDWi5diKpO_fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8-_ufq-5QFJkyNUpYB9R1mVQwmRGdY0nQ", + "e": "AQAB", + "x5c": [ + "MIIC/jCCAeagAwIBAgIJAJtuCSyF4i1FMA0GCSqGSIb3DQEBCwUAMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMjQwNjA5MTkxNzM5WhcNMjkwNjA5MTkxNzM5WjAtMSswKQYDVQQDEyJhY2NvdW50cy5hY2Nlc3Njb250cm9sLndpbmRvd3MubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyfNcG8Ka/b4R7niLqdzlFvzRjrTdl2wTVEtqRWXqDhJAt/KIizVrqe0x3E1tNohmySHAMz3IS4llC41ML5YUDZVg33XR0RMc6UntOq+qCSOkPXdliC3QkwxspGAaJxVzhO5OZuQlMoQNL6/1FgdaR62gfayjLSJepB8M+o7yC8sOtRhatwe9kbO/5QJC54B8ni0ge5i9nANMln+9ZCHeRQYkgl0RSvR/KtfpWrEqAa4K2cyPaDqejOs8G8V0kM/8CLtDWi5diKpO/fvzRJwparEB5hfMdjAyJgdTOqCVUulZdL7tsoHzb8+/ufq+5QFJkyNUpYB9R1mVQwmRGdY0nQIDAQABoyEwHzAdBgNVHQ4EFgQUzF0gtMcVDEn4JoNlDOxvhM8IHBswDQYJKoZIhvcNAQELBQADggEBAJe2muR0H2h3phiZ/v6FD8Yio6niulN9jr7+eC/UJV1M7l5xdHgVL83JbNZjUECDrJ/m+ICY1NbEXfv4fo3sfpU1AwG5GXAhxTrS4zMhH7Hvir3800wCd3ByJ/2vQW1y3orlqR8Q65BN9ayub6BCBTNmtUAOpAWcnP3FnGtIDmAL4APcacK92ZTg8ayVX586U7DDWmI4l7X6xCruK0ic5W2b13k2cay0EalHNWHl+gikqQg6tTGSvM295P6Xy5bQ1I5QtHjVCbm0315T/FylvR8fZhVD+AUCc1DwtOr3Yhm3EXftDb6hP08C4yDhGIDH3Q3+xuWlIA7KQjgljuiT67U=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "x5t": "inEVM76gXEQEonQ0PSQXBO_7HfU", + "n": "q0sct8P8TxXmXX2QXzIhMnwZHdCO96SMFCMtfswF1TpxYqaIFObIhT_zxxpBTsvkYHAxG7CUQ6qVgd_TQhMx0TSZq_X3_0NG6cIRik0g-Woe0gT6tUJ-o6zdtO-6EvoOXovT3YMh8vN1Q5UJV6dudDqjnlTNHH1OxFcU4U6no1R6iILDMci_TGq7I2AJS5i_O9Ptp5NmgDT_kbwZHJz1Abbw4VuOPMFJ2Q1rN9odV9YHKjjowqa3BULVyTvP8FoGUzhoopu6O7oA-ehlO9fhEoSS0zNn0lWXQMZXUF7GSyui12121kIXyll2KlvuETQNdVkeXu0m95g_pnX-8iZ_cw", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIINBmSj+3xdxwwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA2MTIxNjA3MjBaFw0yOTA2MTIxNjA3MjBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrSxy3w/xPFeZdfZBfMiEyfBkd0I73pIwUIy1+zAXVOnFipogU5siFP/PHGkFOy+RgcDEbsJRDqpWB39NCEzHRNJmr9ff/Q0bpwhGKTSD5ah7SBPq1Qn6jrN2077oS+g5ei9PdgyHy83VDlQlXp250OqOeVM0cfU7EVxThTqejVHqIgsMxyL9MarsjYAlLmL870+2nk2aANP+RvBkcnPUBtvDhW448wUnZDWs32h1X1gcqOOjCprcFQtXJO8/wWgZTOGiim7o7ugD56GU71+EShJLTM2fSVZdAxldQXsZLK6LXbXbWQhfKWXYqW+4RNA11WR5e7Sb3mD+mdf7yJn9zAgMBAAGjITAfMB0GA1UdDgQWBBSZbhe/r/sxfv0nYlyrwjx+b6W2RTANBgkqhkiG9w0BAQsFAAOCAQEAoWZ+C/snZySK1KiOsrn1iq7wrVzkuModPMZEshR3SuDIB6+C76fmP42I3UtDVIY5EeE79YjdwDwy86dPZjKVNbP7yUSbJC8uPM1TNMA9s8QpO6RZ63ZZ4i8hcgk6PXgi0PPjX2cmzUSNUa4gS8ibhf7JDu4aF9lUceBsNQghNQfz3tBs1ksJoW3WY5EfW6yMCv1Vim0uBlpnYdlynAd8O+9N2JR9wC+12PwPGrdGQDX3pos8bnmBxM55ueiGoqDH5yGI1h63POlGnpEdqOONT8N4cZNazQ1NswbBQuZMZSfbPXjiiFQ4bktyiXr421KbknQrkRYogi1F2Cjrd4SZJg==" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "x5t": "KQ2tAcrE7lBaVVGBmc5FobgdJo4", + "n": "08xqQ-OBv9jvWmtvWw8g3IkcuDHVOAGCn3K6TXyKie0L6cAyQWNX4vqxbt0cHdaLunrzaFJ2mIGj_qfor8KR_FOFVFOF24FAakB5El96LvsTwlWJNIw4kpf1O_xibycZ_UcDAEqABJfe51JSPh-PxI2sXt0UMapSjvTdnps0Conp11Ay_yupl_h7nawVg0kzw3QDX5-vKTruiHAHr845YwDRW1yJLEgkUPYXdM8d_SrRgqb2RKJEN8D1c4-SUpFHKwGAwLgVYH1cqwADX9el857z_2uKqJoP48l8WqUOfNGdvx79RCgF1NzzRh07EQrk0GJ_EB8eO-EF4YHLPImVtQ", + "e": "AQAB", + "x5c": [ + "MIIC/TCCAeWgAwIBAgIIRf5MUh/1XVIwDQYJKoZIhvcNAQELBQAwLTErMCkGA1UEAxMiYWNjb3VudHMuYWNjZXNzY29udHJvbC53aW5kb3dzLm5ldDAeFw0yNDA3MTAxNjA0NTBaFw0yOTA3MTAxNjA0NTBaMC0xKzApBgNVBAMTImFjY291bnRzLmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDTzGpD44G/2O9aa29bDyDciRy4MdU4AYKfcrpNfIqJ7QvpwDJBY1fi+rFu3Rwd1ou6evNoUnaYgaP+p+ivwpH8U4VUU4XbgUBqQHkSX3ou+xPCVYk0jDiSl/U7/GJvJxn9RwMASoAEl97nUlI+H4/Ejaxe3RQxqlKO9N2emzQKienXUDL/K6mX+HudrBWDSTPDdANfn68pOu6IcAevzjljANFbXIksSCRQ9hd0zx39KtGCpvZEokQ3wPVzj5JSkUcrAYDAuBVgfVyrAANf16XznvP/a4qomg/jyXxapQ580Z2/Hv1EKAXU3PNGHTsRCuTQYn8QHx474QXhgcs8iZW1AgMBAAGjITAfMB0GA1UdDgQWBBQGoURL0sKGdYALEdvfObZ6NEgmJTANBgkqhkiG9w0BAQsFAAOCAQEAl/UkmIa4OvsgULkBmGIZ6HeyJDvVuORphBK9/vpxEFsgnlitwMncBO54uMjJVr63baV490ODSI+ZTiCh7WGM+zrSjllCbVWDxjrdXA1ygHnXX7bXecIQyDmVb5/Hfb7DmQ4MHa3lEwf+pNS5XJeOhPoduRsfYCdD0QbxEADDgqV4FtgYx4I+iAoqbPDPou7wchEu9d3MuFuTMorkTvDLCyTHi2rgBnk9GBf2rArCGyTpvVPGXmxBttqgm9krFRujLj00u9jKUx4YkmAhS9YRddME8+gh6X4qFMxQMhyzkaBxjLs/E+pwMJaUwBqvostwt9+52qrMSUo+jkFgiGCe4Q==" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "x5t": "EHu9neGZBCDyv2IYq8U5JiRMFng", + "n": "w1kH9dFGdaJS8fvQulDssuuNhkczzy1Mo6IiNoC3ih3K-L_VF5TQmSkqXrovWCUlhBCfc1VPR9Cn2G4UP7Sygn0nTqXBY1NFQQZecqwGESJFIuonRqjdlDhNYXjSF_eg63KyuyLV8A-Sn05Ufuc8ax0tyrxPbkOql0pB2hmRhj94iDAFB2LBoxfEgxCG3VT0ascVYW6voTCChs2P65-4RLC-ib1w1FjuACDwsB7KZDxxaUGLfnIoLWUjmw1zCaDRiRvhxB4jQXpB64IFxaYsqxA_x8bj2JEE7qALZ2dZ3fPy9yYSAnRfaTMetgouR9x4SKy4HxUxsADMm_7p9LiRZQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAO8yTjZIibNNMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA1MDYyMzA5MDJaFw0yOTA1MDYyMzA5MDJaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMNZB/XRRnWiUvH70LpQ7LLrjYZHM88tTKOiIjaAt4odyvi/1ReU0JkpKl66L1glJYQQn3NVT0fQp9huFD+0soJ9J06lwWNTRUEGXnKsBhEiRSLqJ0ao3ZQ4TWF40hf3oOtysrsi1fAPkp9OVH7nPGsdLcq8T25DqpdKQdoZkYY/eIgwBQdiwaMXxIMQht1U9GrHFWFur6EwgobNj+ufuESwvom9cNRY7gAg8LAeymQ8cWlBi35yKC1lI5sNcwmg0Ykb4cQeI0F6QeuCBcWmLKsQP8fG49iRBO6gC2dnWd3z8vcmEgJ0X2kzHrYKLkfceEisuB8VMbAAzJv+6fS4kWUCAwEAAaMhMB8wHQYDVR0OBBYEFJ4xtCt3JpPxlUVH7ATgJGM4ofg7MA0GCSqGSIb3DQEBCwUAA4IBAQB9WAEvE3VtO5wIOtN5N/QbIU63H5QPgMW3M9nOs43AhLgwvWupxaiATyMqtK53RPvcxYPe7QwSw/xH9McXii1bOBVmc71AcjlXYfuMJ/0IMEFEUQwZDEwj+vIlg07gWh0hleehyAgMblDUQRRN+b5J+soa9LBBAooY/48F/++y4DiTzKyoWn5cV4H2kdIFVyB43XzJRqDoK1ZhplVLTc1a3K1NL1/qP9rhvtx62YDzfNh4+FTJLu31ALcUbD+Qx2m0U9wuWq3EdUzEen5DeLvhx55YD7V1BASHNYBd8lGhHk97aTw53CMGAuTELvWO+4x7dFM9autw2KvSn76n/4Ql" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "FB8_wii85nv_UW3qrldTvWwg-rE", + "x5t": "FB8_wii85nv_UW3qrldTvWwg-rE", + "n": "vusbA5UBNtCB0U2RmyQOCE-8fWl8bzCQXm3V5Nd7oockcyCpqXOWfhVNJD-Ifb5_zAmxRgHvRdfpA2btaqZiit5XaFYngtRK6mVxCcnOEgwxQGX9DLM5plXWtGTf_DF1FATBidFlM8KgicTS3MTyKZNrnTz0JD7ISxwV0TgSEiRrsm7eVsumuNYNW30Yb38DRDTei9U1YR0YDmdZyuf-OKTllxKH_BO-aj8Gkxcnkdriih2CINF6M6oASOHTJYO7P8CQE1DX2y2Zq7xxVvzm4IClk7WDdzuAoC-ZiKvDaU5plSyrnH3_VgjJrzXtuGN-HEd4Vg89h_2rE74cN5KRtQ", + "e": "AQAB", + "x5c": [ + "MIIC6jCCAdKgAwIBAgIJAJOO92n+BJBLMA0GCSqGSIb3DQEBCwUAMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czAeFw0yNDA2MTAxODA3NDVaFw0yOTA2MTAxODA3NDVaMCMxITAfBgNVBAMTGGxvZ2luLm1pY3Jvc29mdG9ubGluZS51czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL7rGwOVATbQgdFNkZskDghPvH1pfG8wkF5t1eTXe6KHJHMgqalzln4VTSQ/iH2+f8wJsUYB70XX6QNm7WqmYoreV2hWJ4LUSuplcQnJzhIMMUBl/QyzOaZV1rRk3/wxdRQEwYnRZTPCoInE0tzE8imTa5089CQ+yEscFdE4EhIka7Ju3lbLprjWDVt9GG9/A0Q03ovVNWEdGA5nWcrn/jik5ZcSh/wTvmo/BpMXJ5Ha4oodgiDRejOqAEjh0yWDuz/AkBNQ19stmau8cVb85uCApZO1g3c7gKAvmYirw2lOaZUsq5x9/1YIya817bhjfhxHeFYPPYf9qxO+HDeSkbUCAwEAAaMhMB8wHQYDVR0OBBYEFLBW6P0A+qHESOFg8Rgxqp38myYtMA0GCSqGSIb3DQEBCwUAA4IBAQAsZzkzk8w7RR3KCHOY+XLn3R2NanL/j+WILdOHnJn9Ot1VbG868MFQgwMp8Y2y7Kj5RekknY6EGcNuJi4rLgq5u1LSB/IoNPCs7l3MhRQqoedJX4sDNf4NfTVHK+4GNSQqP60eBoxClRexIbKcHJ0x57Ww/S9NNWtldBIfB7egoSj6UVcTHRLWZyPoZsOXHY4bYOf8ANNg21jT1KWwOXSWUx60v7tVxEXs8XAEUnmuMbuh3yAnjv3UoRdl7wcaQ5jq2/+vaAWZm0WlWN3CCY3y2mE0OZZg9HRCQu+o+58wt658sDIpP7PXGjyA5h23W9+i8QtyQ1PtqCXKj8zktivW" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "x5t": "ZZ8JkzXRCgYMWcMYdOn9sdDBeUA", + "n": "iJd0N795eVyYQvWe417HOF_GHlRgOsPZRh1KwNHyWP_WKrjlOl8ftPAs-Sspv-s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk_Fa4K7sutgQdyxKehhRnk4hAIc-mUM9ROrkyr4dIi-Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB-uDX2Z_hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB-wBEe-SF5bKVNSfExcsiL0KzV_iQkKQNALJrakX4Mw-hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F-y1h873iOkYWiebNIu5TeTjVww", + "e": "AQAB", + "x5c": [ + "MIIC6TCCAdGgAwIBAgIIHxgoSKq7mWgwDQYJKoZIhvcNAQELBQAwIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMB4XDTI0MDYxMDE4MjgyOFoXDTI5MDYxMDE4MjgyOFowIzEhMB8GA1UEAxMYbG9naW4ubWljcm9zb2Z0b25saW5lLnVzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAiJd0N795eVyYQvWe417HOF/GHlRgOsPZRh1KwNHyWP/WKrjlOl8ftPAs+Sspv+s8v68TilHYkY9pjUdEkxBvBolJiPP6ntAKIKk/Fa4K7sutgQdyxKehhRnk4hAIc+mUM9ROrkyr4dIi+Au9T7aWaBSG5dCffXQBQ1DBVbIUMNOwr4ewQYeb49ujxzE6dPiCB+uDX2Z/hjy9M8wMrHS8e2vKDYqx3AJ3xyiDjIDB+wBEe+SF5bKVNSfExcsiL0KzV/iQkKQNALJrakX4Mw+hC3ssv7q3NQcza9kpyew0TKytSOcJcIheX9Cse22F+y1h873iOkYWiebNIu5TeTjVwwIDAQABoyEwHzAdBgNVHQ4EFgQUcLvbIYVCbexuF1KXcKysM8kS6EMwDQYJKoZIhvcNAQELBQADggEBAF95Wf/yAfmHksmL42JiCemjsHN0KlZ2NsGTj2+zbDXbttj8zm+ZA74bPlAWI5aFvKfxxpC3Chfi26+GhKVeVRA65KyokTulQzE+BWbqphQZoH6Iz07J3GB3uUthPQbedtj6SDD/zE4jcmhmrY8o0lU5zJhkp9T5f8644ZR6rJRIXpFbDwmbsFM5H4Nz7D5FG+A4uYumICoTaiQjJ+cu/k8sDM8ut6R2cGmwlRMIGzD8HzNeGuaRtXsFqCGAI+qRbW29hJoFNZxhQBeFRDdBvwbNIa/o6ZAzKq81E4SdV1d33oM3vWDMBlR3b46a1d+Unm1Ou8uJ2yDfqMrZ7/NGNV8=" + ], + "issuer": "https://login.microsoftonline.com/{tenantid}/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "dGtHQMhGltJUCcH_SQW64nEUoYE", + "x5t": "dGtHQMhGltJUCcH_SQW64nEUoYE", + "n": "io_Qh_kyYBnXCXPV54XbUZheP_fpWo5M0-_aWJQ6i-CebDHQVpxHurahUYj446qEvUBFK-goUEDU1Ah87F_KXNDQhsJq0F422joJPIzsHSsed_k0KlYnkJgCeUC8yHmtgSnNjH7jCnnBZ6Oznt0rdEw9MVd_2ofWgoA28XRUQ_arpXgGo8EWSPWuLGsG3cKTsSVW-1d_JSZ56S73j5YBDQz11ZPVm13nWohrGEgPBgswCCLUsZod0t1oTiRmKihRom-FhWvsfFixUZ4D39XSk51UjWttu1gnhhxhV7PVqlaqbvQ1D2urlpgMnAgyeQrIUC-3L-fN6hwD_1NZCaQdeQ", + "e": "AQAB", + "x5c": [ + "MIIDCzCCAfOgAwIBAgIRAKdbZc1Eb0WDFn5HsLQuwwYwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MDcxNDE1MDI1MFoXDTI5MDcxNDE1MDI1MFowKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAio/Qh/kyYBnXCXPV54XbUZheP/fpWo5M0+/aWJQ6i+CebDHQVpxHurahUYj446qEvUBFK+goUEDU1Ah87F/KXNDQhsJq0F422joJPIzsHSsed/k0KlYnkJgCeUC8yHmtgSnNjH7jCnnBZ6Oznt0rdEw9MVd/2ofWgoA28XRUQ/arpXgGo8EWSPWuLGsG3cKTsSVW+1d/JSZ56S73j5YBDQz11ZPVm13nWohrGEgPBgswCCLUsZod0t1oTiRmKihRom+FhWvsfFixUZ4D39XSk51UjWttu1gnhhxhV7PVqlaqbvQ1D2urlpgMnAgyeQrIUC+3L+fN6hwD/1NZCaQdeQIDAQABoy4wLDAdBgNVHQ4EFgQUS8ytdfEdWAsFJApZy/6lA7wlfgowCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQBDnyg3sAC3S64Pm4xA81r2kts96usCRu7tF34f3RJX7Met+rJMrllpRT8zVTzFTaPjHsJvhl5F/ApD0lZN6noy7UwNjbnMoC/lYluPLDuQE4ClstsgpNBdSNF0l+tWk085sIM7LF3wAuf17Yp5jIXCyokbbDJb5+XpNGZm4ukTLADajk/jk76z7p94shgV1XMla3fV+1d7jDL6UlbIvXNUSp3swvSLQPv90sSI2OUwTTulNZDokmeWtLUedTTIpnu9y+vLJWbKiwtenYbj3zM7VN/Qr5aXl4w3Ajx+QKnRydv1se8ycMabu28OFXgP92AsY1/NW4BF6321OOq2OmbC" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "pb_TKRXVJY-27vIG_A81PMH-cdY", + "x5t": "pb_TKRXVJY-27vIG_A81PMH-cdY", + "n": "iM24cYc714exgvGQeAuw6pqYqkSf7NEuLug5jCYcGqa2APSSjzks5h7en-uEEnE0q03VeaJB6PWxaE3GTfGKXzr-sPudGCTTOsgnY3t4ms3DLeyhZvWi5ADc4JtpLBQOxYm1f4ReGwryZqsOHdvqNiYn7B7PyN_3dVbUuXWaueCJ3hhW5JyXkRGD75cOsgOm7GU3tYtOcxm29yjOzNcQXOiL_fChEz6G6bjOHzFYISgv5m7TffaOEFF4T4RoP4AQ35zvxjHx8XkBQPTz661TjTN1h_mYsFEwa2cDcErjJ4dJTdKSkM-VFPDklcSXsrDhkOw42ZeuKAoQTVep5EJ71w", + "e": "AQAB", + "x5c": [ + "MIIDCjCCAfKgAwIBAgIQNas2IybvbYSgWXpOzctlSDANBgkqhkiG9w0BAQsFADApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwHhcNMjQwNzAxMTgzMzA1WhcNMjkwNzAxMTgzMzA1WjApMScwJQYDVQQDEx5MaXZlIElEIFNUUyBTaWduaW5nIFB1YmxpYyBLZXkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzbhxhzvXh7GC8ZB4C7DqmpiqRJ/s0S4u6DmMJhwaprYA9JKPOSzmHt6f64QScTSrTdV5okHo9bFoTcZN8YpfOv6w+50YJNM6yCdje3iazcMt7KFm9aLkANzgm2ksFA7FibV/hF4bCvJmqw4d2+o2JifsHs/I3/d1VtS5dZq54IneGFbknJeREYPvlw6yA6bsZTe1i05zGbb3KM7M1xBc6Iv98KETPobpuM4fMVghKC/mbtN99o4QUXhPhGg/gBDfnO/GMfHxeQFA9PPrrVONM3WH+ZiwUTBrZwNwSuMnh0lN0pKQz5UU8OSVxJeysOGQ7DjZl64oChBNV6nkQnvXAgMBAAGjLjAsMB0GA1UdDgQWBBRsbUU+6mjS1sX/3Ek+xKEA6JTeLDALBgNVHQ8EBAMCAsQwDQYJKoZIhvcNAQELBQADggEBACO1MS24nE70L0Tcw4NRv80uZ8b5OWAfsAO+AN7zwXeo6J7TN/sslMuQ9FtL3Coot2ItdYaFHmfzKijuCV17EiWdXXccwoGEZqp3y2gvyYCof2OVQK4/KZVPUhI8wg2kR8dn09B9fdiMmwqd2+ZezWbgvSz1fQz5gZCg5FbFFojYvQL65bIq3tUZBtAT7ixrcGOfFbYzZbpi4mJdJItidd3Oh+TXfexzRL5Cw7Zn4LGlUVUOwildBfYtB+Fr022wutr/adxjJV7wgr6AxaTlls/hQz6+TOs8Vmyeb8KsU9CJZRXPIBKvZwAyMJsDZ3l4x+XPAZYQo3i6Oa4F5ROR9ZM=" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + }, + { + "kty": "RSA", + "use": "sig", + "kid": "yBm7yTZhZ3SWV3-9inQcQk1X51U", + "x5t": "yBm7yTZhZ3SWV3-9inQcQk1X51U", + "n": "nwNwsp54S9SUESzWUZXc0dY19bOVn4smmRSxANxPblU0nQEBpDPumlBVYmHI3XXVIshrh2DAl4BSVfQhVKLCu35Vyv7_P9cLvmqM_dvIHEjtrQPPFIBlH6fitB4v5zs7i7_zV-mTteGsNoUWg-TtHHKekJBrrBxoJ633vvaZ9AEFP8OdZoVGjXW1Wb76nczV8uhjgF9u69XrOPVrYB7YcxtiA-jRzn8AQRt8SfkrIvEjDL5ejtxRNyucz8dFzmbrCazoUY3oeei6UHjdtFgiODs4KE29e6p1Lm4CexjkcIrFWXkoxytOKEsB5zCGq8pQeI-tGmoCBhVnhNw7u5okjQ", + "e": "AQAB", + "x5c": [ + "MIIDCzCCAfOgAwIBAgIRAMtLWPmqFNLXNg6BbZWr9EAwDQYJKoZIhvcNAQELBQAwKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MB4XDTI0MDYxODIyMzE1MFoXDTI5MDYxODIyMzE1MFowKTEnMCUGA1UEAxMeTGl2ZSBJRCBTVFMgU2lnbmluZyBQdWJsaWMgS2V5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnwNwsp54S9SUESzWUZXc0dY19bOVn4smmRSxANxPblU0nQEBpDPumlBVYmHI3XXVIshrh2DAl4BSVfQhVKLCu35Vyv7/P9cLvmqM/dvIHEjtrQPPFIBlH6fitB4v5zs7i7/zV+mTteGsNoUWg+TtHHKekJBrrBxoJ633vvaZ9AEFP8OdZoVGjXW1Wb76nczV8uhjgF9u69XrOPVrYB7YcxtiA+jRzn8AQRt8SfkrIvEjDL5ejtxRNyucz8dFzmbrCazoUY3oeei6UHjdtFgiODs4KE29e6p1Lm4CexjkcIrFWXkoxytOKEsB5zCGq8pQeI+tGmoCBhVnhNw7u5okjQIDAQABoy4wLDAdBgNVHQ4EFgQUFGb4FaXDu89wqG9A6JK1xLUMKPUwCwYDVR0PBAQDAgLEMA0GCSqGSIb3DQEBCwUAA4IBAQAw3cpDyl03Kgka9P2BJR6xU+C+IiWpJVLbLbdvBepqGI1/NqXCrG7E4INS5oRsFVO8DuYXws7Ko5kKCTV+iqkGngtG9b/JFP8QBcRrhngHTnE8EevwLkDtqFvpBdNnzTmOJDP4FdtYRuucJqx7aLE1MXr2jEkKfY7YLu2YEmOG6hnZfqWeCRm+g9eUolbhexllsdtj3bi9V9c8anXPLUsEeY/BRT7n4TBGJBWDD9kYEgoMKPLp58Om8aY6BucKN6vjf/v9RR//2ggCXX+qrZP3ebj9cXI6dWtgn5WwkBTfufIXnbbrzyCp/jdCP7q8SXbG2MeFiqKLKul5q5neiIdm" + ], + "issuer": "https://login.microsoftonline.com/9188040d-6c67-4c5b-b112-36a304b66dad/v2.0" + } + ] + } + """; public static OpenIdConnectConfiguration AADCommonV2Config { get @@ -372,6 +586,7 @@ public static OpenIdConnectConfiguration AADCommonV2Config config.AdditionalData.Add("cloud_graph_host_name", "graph.windows.net"); config.AdditionalData.Add("msgraph_host", "graph.microsoft.com"); config.AdditionalData.Add("rbac_url", "https://pas.windows.net"); + config.JsonWebKeySet = JsonWebKeySet.Create(AADCommonV2JwksString); return config; } diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs index 22454614f5..2e3c38f305 100644 --- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectSerializationTests.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using Microsoft.IdentityModel.TestUtils; using Microsoft.IdentityModel.Tokens.Json.Tests; using Xunit; @@ -23,6 +24,7 @@ public void Deserialize(OpenIdConnectTheoryData theoryData) OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration(theoryData.Json); OpenIdConnectConfiguration configurationUpperCase = new OpenIdConnectConfiguration(JsonUtilities.SetPropertiesToUpperCase(theoryData.Json)); theoryData.ExpectedException.ProcessNoException(context); + context.PropertiesToIgnoreWhenComparing.Add(typeof(OpenIdConnectConfiguration), new List { "JsonWebKeySet" }); IdentityComparer.AreEqual(configuration, theoryData.CompareTo, context); IdentityComparer.AreEqual(configurationUpperCase, theoryData.CompareTo, context); diff --git a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs index 7d71601577..711d00ce46 100644 --- a/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs +++ b/test/Microsoft.IdentityModel.Protocols.Tests/ExtensibilityTests.cs @@ -69,28 +69,34 @@ public void GetMetadataTest(DocumentRetrieverTheoryData theoryData) } [Fact] - public void ConfigurationManagerUsingCustomClass() + public async Task ConfigurationManagerUsingCustomClass() { var docRetriever = new FileDocumentRetriever(); var configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); - var context = new CompareContext($"{this}.GetConfiguration"); + var context = new CompareContext($"{this}.ConfigurationManagerUsingCustomClass"); var configuration = configManager.GetConfigurationAsync().Result; configManager.MetadataAddress = "IssuerMetadata.json"; var configuration2 = configManager.GetConfigurationAsync().Result; - if (!IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) + if (!IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer, context)) context.Diffs.Add("!IdentityComparer.AreEqual(configuration, configuration2)"); // AutomaticRefreshInterval should pick up new bits. configManager = new ConfigurationManager("IssuerMetadata.json", new IssuerConfigurationRetriever(), docRetriever); configManager.RequestRefresh(); configuration = configManager.GetConfigurationAsync().Result; - TestUtilities.SetField(configManager, "_lastRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); + TestUtilities.SetField(configManager, "_lastRequestRefresh", DateTimeOffset.UtcNow - TimeSpan.FromHours(1)); configManager.MetadataAddress = "IssuerMetadata2.json"; configManager.RequestRefresh(); - configuration2 = configManager.GetConfigurationAsync().Result; - if (IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) - context.Diffs.Add("IdentityComparer.AreEqual(configuration, configuration2)"); + + // Wait for the refresh to complete. + await Task.Delay(250).ContinueWith(_ => + { + + configuration2 = configManager.GetConfigurationAsync().GetAwaiter().GetResult(); + if (IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)) + context.Diffs.Add("IdentityComparer.AreEqual(configuration.Issuer, configuration2.Issuer)"); + }); TestUtilities.AssertFailIfErrors(context); } diff --git a/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs new file mode 100644 index 0000000000..1a1f81fe6f --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/InMemoryDocumentRetriever.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; + +namespace Microsoft.IdentityModel.TestUtils +{ + /// + /// Returns a string set in the constructor. + /// Simplifies testing. + /// + public class InMemoryDocumentRetriever : IDocumentRetriever + { + private readonly IDictionary _configurations; + + /// + /// Initializes a new instance of the class. + /// + public InMemoryDocumentRetriever(IDictionary configuration) + { + _configurations = configuration; + } + + /// + /// Returns the document passed in constructor in dictionary./> + /// + /// Fully qualified path to a file. Ignored for now. + /// Ignored for now. + /// UTF8 decoding of bytes in the file. + public async Task GetDocumentAsync(string address, CancellationToken cancel) + { + return await Task.FromResult(_configurations[address]).ConfigureAwait(false); + } + } +} diff --git a/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs b/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs index dd02c50729..76dee7c906 100644 --- a/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs +++ b/test/Microsoft.IdentityModel.TestUtils/SampleListener.cs @@ -8,7 +8,7 @@ namespace Microsoft.IdentityModel.TestUtils { public class SampleListener : EventListener { - public string TraceBuffer { get; set; } + public string TraceBuffer { get; set; } = string.Empty; protected override void OnEventWritten(EventWrittenEventArgs eventData) {