diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/InternalsVisibleTo.cs b/src/Microsoft.IdentityModel.JsonWebTokens/InternalsVisibleTo.cs index d5dbc25a51..5c33df843c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/InternalsVisibleTo.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/InternalsVisibleTo.cs @@ -3,3 +3,4 @@ [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.Validators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] [assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.JsonWebTokens.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] +[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("Microsoft.IdentityModel.TestUtils, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b5fc90e7027f67871e773a8fde8938c81dd402ba65b9201d60593e96c492651e889cc13f1415ebb53fac1131ae0bd333c5ee6021672d9718ea31a8aebd0da0072f25d87dba6fc90ffd598ed4da35e44c398c454307e8e33b8426143daec9f596836f97c8f74750e5975c64e2189f45def46b2a2b1247adc3652bf5c308055da9")] diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.DecryptToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.DecryptToken.cs new file mode 100644 index 0000000000..f7e7922bc2 --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.DecryptToken.cs @@ -0,0 +1,254 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ +#nullable enable + public partial class JsonWebTokenHandler : TokenHandler + { + /// + /// Decrypts a JWE and returns the clear text. + /// + /// The JWE that contains the cypher text. + /// The to be used for validating the token. + /// The to be used for validating the token. + /// + /// The decoded / cleartext contents of the JWE. + /// Returned inside if is null. + /// Returned inside if is null. + /// Returned inside if is null or empty. + /// Returned inside if the decompression failed. + /// Returned inside if is not null AND the decryption fails. + /// Returned inside if the JWE was not able to be decrypted. + internal TokenDecryptionResult DecryptToken( + JsonWebToken jwtToken, + ValidationParameters validationParameters, + BaseConfiguration configuration, + CallContext? callContext) + { + if (jwtToken == null) + return TokenDecryptionResult.NullParameterFailure(jwtToken, nameof(jwtToken)); + + if (validationParameters == null) + return TokenDecryptionResult.NullParameterFailure(jwtToken, nameof(validationParameters)); + + if (string.IsNullOrEmpty(jwtToken.Enc)) + return new TokenDecryptionResult( + jwtToken, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10612), + typeof(SecurityTokenException), + new System.Diagnostics.StackFrame())); + + var keysOrExceptionDetail = GetContentEncryptionKeys(jwtToken, validationParameters, configuration, callContext); + if (keysOrExceptionDetail.Item2 != null) // ExceptionDetail returned + return new TokenDecryptionResult( + jwtToken, + ValidationFailureType.TokenDecryptionFailed, + keysOrExceptionDetail.Item2); + + var keys = keysOrExceptionDetail.Item1; + if (keys == null) + return new TokenDecryptionResult( + jwtToken, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10609, + LogHelper.MarkAsSecurityArtifact(jwtToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenDecryptionFailedException), + new System.Diagnostics.StackFrame())); + + return JwtTokenUtilities.DecryptJwtToken( + jwtToken, + validationParameters, + new JwtTokenDecryptionParameters + { + DecompressionFunction = JwtTokenUtilities.DecompressToken, + Keys = keys, + MaximumDeflateSize = MaximumTokenSizeInBytes + }, + callContext); + } + + internal (IList?, ExceptionDetail?) GetContentEncryptionKeys(JsonWebToken jwtToken, ValidationParameters validationParameters, BaseConfiguration configuration, CallContext? callContext) + { + IList? keys = null; + + // First we check to see if the caller has set a custom decryption resolver on VP for the call, if so any keys set on VP and keys in Configuration are ignored. + // If no custom decryption resolver is set, we'll check to see if they've set some static decryption keys on VP. If a key is found, we ignore configuration. + // If no key found in VP, we'll check the configuration. + if (validationParameters.TokenDecryptionKeyResolver != null) + { + keys = validationParameters.TokenDecryptionKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, callContext); + } + else + { + var key = ResolveTokenDecryptionKey(jwtToken.EncodedToken, jwtToken, validationParameters, callContext); + if (key != null) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10904, key); + } + else if (configuration != null) + { + key = ResolveTokenDecryptionKeyFromConfig(jwtToken, configuration); + if (key != null && LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10905, key); + } + + if (key != null) + keys = [key]; + } + + // on decryption for ECDH-ES, we get the public key from the EPK value see: https://datatracker.ietf.org/doc/html/rfc7518#appendix-C + // we need the ECDSASecurityKey for the receiver, use TokenValidationParameters.TokenDecryptionKey + + // control gets here if: + // 1. User specified delegate: TokenDecryptionKeyResolver returned null + // 2. ResolveTokenDecryptionKey returned null + // 3. ResolveTokenDecryptionKeyFromConfig returned null + // Try all the keys. This is the degenerate case, not concerned about perf. + if (keys == null) + { + keys = validationParameters.TokenDecryptionKeys; + if (configuration != null) + { + if (configuration.TokenDecryptionKeys is not List configurationKeys) + configurationKeys = configuration.TokenDecryptionKeys.ToList(); + + if (keys != null) + { + if (keys is List keysList) + keysList.AddRange(configurationKeys); + else + keys = keys.Concat(configurationKeys).ToList(); + } + else + keys = configurationKeys; + } + + } + + if (jwtToken.Alg.Equals(JwtConstants.DirectKeyUseAlg, StringComparison.Ordinal) + || jwtToken.Alg.Equals(SecurityAlgorithms.EcdhEs, StringComparison.Ordinal)) + return (keys, null); + + if (keys is null) + return (keys, null); // Cannot iterate over null. + + var unwrappedKeys = new List(); + // keep track of exceptions thrown, keys that were tried + StringBuilder? exceptionStrings = null; + StringBuilder? keysAttempted = null; + for (int i = 0; i < keys.Count; i++) + { + var key = keys[i]; + + try + { +#if NET472 || NET6_0_OR_GREATER + if (SupportedAlgorithms.EcdsaWrapAlgorithms.Contains(jwtToken.Alg)) + { + // on decryption we get the public key from the EPK value see: https://datatracker.ietf.org/doc/html/rfc7518#appendix-C + var ecdhKeyExchangeProvider = new EcdhKeyExchangeProvider( + key as ECDsaSecurityKey, + validationParameters.EphemeralDecryptionKey as ECDsaSecurityKey, + jwtToken.Alg, + jwtToken.Enc); + jwtToken.TryGetHeaderValue(JwtHeaderParameterNames.Apu, out string apu); + jwtToken.TryGetHeaderValue(JwtHeaderParameterNames.Apv, out string apv); + SecurityKey kdf = ecdhKeyExchangeProvider.GenerateKdf(apu, apv); + var kwp = key.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(kdf, ecdhKeyExchangeProvider.GetEncryptionAlgorithm()); + var unwrappedKey = kwp.UnwrapKey(Base64UrlEncoder.DecodeBytes(jwtToken.EncryptedKey)); + unwrappedKeys.Add(new SymmetricSecurityKey(unwrappedKey)); + } + else +#endif + if (key.CryptoProviderFactory.IsSupportedAlgorithm(jwtToken.Alg, key)) + { + var kwp = key.CryptoProviderFactory.CreateKeyWrapProviderForUnwrap(key, jwtToken.Alg); + var unwrappedKey = kwp.UnwrapKey(jwtToken.EncryptedKeyBytes); + unwrappedKeys.Add(new SymmetricSecurityKey(unwrappedKey)); + } + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } + + (keysAttempted ??= new StringBuilder()).AppendLine(key.ToString()); + } + + if (unwrappedKeys.Count > 0 && exceptionStrings is null) + return (unwrappedKeys, null); + else + { + ExceptionDetail exceptionDetail = new( + new MessageDetail( + TokenLogMessages.IDX10618, + keysAttempted?.ToString() ?? "", + exceptionStrings?.ToString() ?? "", + LogHelper.MarkAsSecurityArtifact(jwtToken, JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenKeyWrapException), + new System.Diagnostics.StackFrame()); + return (null, exceptionDetail); + } + } + + /// + /// Returns a to use when decrypting a JWE. + /// + /// The the token that is being decrypted. + /// The that is being decrypted. + /// The to be used for validating the token. + /// The call context used for logging. + /// A to use for signature validation. + /// If key fails to resolve, then null is returned. + internal virtual SecurityKey? ResolveTokenDecryptionKey(string token, JsonWebToken jwtToken, ValidationParameters validationParameters, CallContext? callContext) + { + if (jwtToken == null || validationParameters == null) + return null; + + if (!string.IsNullOrEmpty(jwtToken.Kid) && validationParameters.TokenDecryptionKeys != null) + { + for (int i = 0; i < validationParameters.TokenDecryptionKeys.Count; i++) + { + var key = validationParameters.TokenDecryptionKeys[i]; + if (key != null && string.Equals(key.KeyId, jwtToken.Kid, GetStringComparisonRuleIf509OrECDsa(key))) + return key; + } + } + + if (!string.IsNullOrEmpty(jwtToken.X5t) && validationParameters.TokenDecryptionKeys != null) + { + for(int i = 0; i < validationParameters.TokenDecryptionKeys.Count; i++) + { + var key = validationParameters.TokenDecryptionKeys[i]; + + if (key != null && string.Equals(key.KeyId, jwtToken.X5t, GetStringComparisonRuleIf509(key))) + return key; + + var x509Key = key as X509SecurityKey; + if (x509Key != null && string.Equals(x509Key.X5t, jwtToken.X5t, StringComparison.OrdinalIgnoreCase)) + return key; + } + } + + return null; + } +#nullable restore + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.DecryptTokenResult.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.DecryptTokenResult.cs new file mode 100644 index 0000000000..ab098857d8 --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.DecryptTokenResult.cs @@ -0,0 +1,151 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text; +using Microsoft.IdentityModel.Tokens; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Abstractions; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ + public partial class JwtTokenUtilities + { + /// + /// Decrypts a JWT token. + /// + /// The JWT token to decrypt. + /// The to be used for validating the token. + /// The decryption parameters container. + /// The call context used for logging. + /// The decrypted, and if the 'zip' claim is set, decompressed string representation of the token. + internal static TokenDecryptionResult DecryptJwtToken( + JsonWebToken jsonWebToken, + ValidationParameters validationParameters, + JwtTokenDecryptionParameters decryptionParameters, + CallContext callContext) + { + if (validationParameters == null) + return new TokenDecryptionResult( + jsonWebToken, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10000, + nameof(validationParameters)), + typeof(ArgumentNullException), + new System.Diagnostics.StackFrame())); + + if (decryptionParameters == null) + return new TokenDecryptionResult( + jsonWebToken, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10000, + nameof(decryptionParameters)), + typeof(ArgumentNullException), + new System.Diagnostics.StackFrame())); + + bool decryptionSucceeded = false; + bool algorithmNotSupportedByCryptoProvider = false; + byte[] decryptedTokenBytes = null; + + // keep track of exceptions thrown, keys that were tried + StringBuilder exceptionStrings = null; + StringBuilder keysAttempted = null; + string zipAlgorithm = null; + foreach (SecurityKey key in decryptionParameters.Keys) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (cryptoProviderFactory == null) + { + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(TokenLogMessages.IDX10607, key); + + continue; + } + + try + { + if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Enc, key)) + { + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(TokenLogMessages.IDX10611, LogHelper.MarkAsNonPII(decryptionParameters.Enc), key); + + algorithmNotSupportedByCryptoProvider = true; + continue; + } + + AlgorithmValidationResult result = validationParameters.AlgorithmValidator(zipAlgorithm, key, jsonWebToken, validationParameters, callContext); + if (!result.IsValid) + { + (exceptionStrings ??= new StringBuilder()).AppendLine(result.ExceptionDetail.MessageDetail.Message); + continue; + } + + decryptedTokenBytes = DecryptToken( + cryptoProviderFactory, + key, + jsonWebToken.Enc, + jsonWebToken.CipherTextBytes, + jsonWebToken.HeaderAsciiBytes, + jsonWebToken.InitializationVectorBytes, + jsonWebToken.AuthenticationTagBytes); + + zipAlgorithm = jsonWebToken.Zip; + decryptionSucceeded = true; + break; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } + + if (key != null) + (keysAttempted ??= new StringBuilder()).AppendLine(key.ToString()); + } + + if (!decryptionSucceeded) + return new TokenDecryptionResult( + jsonWebToken, + ValidationFailureType.TokenDecryptionFailed, + GetDecryptionExceptionDetail( + decryptionParameters, + algorithmNotSupportedByCryptoProvider, + exceptionStrings, + keysAttempted, + callContext)); + + try + { + string decodedString; + if (string.IsNullOrEmpty(zipAlgorithm)) + decodedString = Encoding.UTF8.GetString(decryptedTokenBytes); + else + decodedString = decryptionParameters.DecompressionFunction(decryptedTokenBytes, zipAlgorithm, decryptionParameters.MaximumDeflateSize); + + return new TokenDecryptionResult(decodedString, jsonWebToken); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenDecryptionResult( + jsonWebToken, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10679, + zipAlgorithm), + typeof(SecurityTokenDecompressionFailedException), + new StackFrame(), + ex)); + } + } + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs index 608ae36bf7..0587d380f1 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Diagnostics; using System.Globalization; using System.Security.Claims; using System.Security.Cryptography; @@ -265,7 +266,7 @@ internal static string DecryptJwtToken( try { // The JsonWebTokenHandler will set the JsonWebToken and those values will be used. - // The JwtSecurityTokenHandler will calculate values and set the values on DecrytionParameters. + // The JwtSecurityTokenHandler will calculate values and set the values on DecryptionParameters. // JsonWebToken from JsonWebTokenHandler if (securityToken is JsonWebToken jsonWebToken) @@ -329,7 +330,14 @@ internal static string DecryptJwtToken( (keysAttempted ??= new StringBuilder()).AppendLine(key.ToString()); } - ValidateDecryption(decryptionParameters, decryptionSucceeded, algorithmNotSupportedByCryptoProvider, exceptionStrings, keysAttempted); + if (!decryptionSucceeded) + throw GetDecryptionExceptionDetail( + decryptionParameters, + algorithmNotSupportedByCryptoProvider, + exceptionStrings, + keysAttempted, + null).GetException(); + try { if (string.IsNullOrEmpty(zipAlgorithm)) @@ -343,16 +351,42 @@ internal static string DecryptJwtToken( } } - private static void ValidateDecryption(JwtTokenDecryptionParameters decryptionParameters, bool decryptionSucceeded, bool algorithmNotSupportedByCryptoProvider, StringBuilder exceptionStrings, StringBuilder keysAttempted) + private static ExceptionDetail GetDecryptionExceptionDetail( + JwtTokenDecryptionParameters decryptionParameters, + bool algorithmNotSupportedByCryptoProvider, + StringBuilder exceptionStrings, + StringBuilder keysAttempted, +#pragma warning disable CA1801 // Review unused parameters + CallContext callContext) +#pragma warning restore CA1801 // Review unused parameters { - if (!decryptionSucceeded && keysAttempted is not null) - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10603, keysAttempted, (object)exceptionStrings ?? "", LogHelper.MarkAsSecurityArtifact(decryptionParameters.EncodedToken, SafeLogJwtToken)))); - - if (!decryptionSucceeded && algorithmNotSupportedByCryptoProvider) - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10619, LogHelper.MarkAsNonPII(decryptionParameters.Alg), LogHelper.MarkAsNonPII(decryptionParameters.Enc)))); - - if (!decryptionSucceeded) - throw LogHelper.LogExceptionMessage(new SecurityTokenDecryptionFailedException(LogHelper.FormatInvariant(TokenLogMessages.IDX10609, LogHelper.MarkAsSecurityArtifact(decryptionParameters.EncodedToken, SafeLogJwtToken)))); + if (keysAttempted is not null) + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10603, + keysAttempted.ToString(), + exceptionStrings?.ToString() ?? string.Empty, + LogHelper.MarkAsSecurityArtifact(decryptionParameters.EncodedToken, SafeLogJwtToken)), + typeof(SecurityTokenDecryptionFailedException), + new StackFrame(true), + null); + else if (algorithmNotSupportedByCryptoProvider) + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10619, + LogHelper.MarkAsNonPII(decryptionParameters.Alg), + LogHelper.MarkAsNonPII(decryptionParameters.Enc)), + typeof(SecurityTokenDecryptionFailedException), + new StackFrame(true), + null); + else + return new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10609, + LogHelper.MarkAsSecurityArtifact(decryptionParameters.EncodedToken, SafeLogJwtToken)), + typeof(SecurityTokenDecryptionFailedException), + new StackFrame(true), + null); } private static byte[] DecryptToken(CryptoProviderFactory cryptoProviderFactory, SecurityKey key, string encAlg, byte[] ciphertext, byte[] headerAscii, byte[] initializationVector, byte[] authenticationTag) diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index db5a79f174..5a1eac59c8 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -169,4 +169,17 @@ namespace Microsoft.IdentityModel.Tokens /// The to be used for validating the token. /// The transformed . public delegate SecurityToken TransformBeforeSignatureValidation(SecurityToken token, TokenValidationParameters validationParameters); + +#nullable enable + /// + /// Resolves the decryption key for the security token. + /// + /// The string representation of the token to be decrypted. + /// The to be decrypted, which is null by default. + /// The key identifier, which may be null. + /// The to be used for validating the token. + /// The to be used for logging. + /// The used to decrypt the token. + internal delegate IList ResolveTokenDecryptionKeyDelegate(string token, SecurityToken securityToken, string kid, ValidationParameters validationParameters, CallContext? callContext); +#nullable restore } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs index 1ab311c9e8..a8da23d694 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { @@ -50,6 +51,13 @@ public Exception GetException() return Activator.CreateInstance(ExceptionType, MessageDetail.Message) as Exception; } + internal static ExceptionDetail NullParameter(string parameterName) => new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(parameterName)), + typeof(ArgumentNullException), + new StackFrame()); + /// /// Gets the type of exception that occurred. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenDecryptionResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenDecryptionResult.cs new file mode 100644 index 0000000000..99488e75ba --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenDecryptionResult.cs @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of decrypting a securityToken in clear text. + /// The contains a collection of for each step in the token validation. + /// + internal class TokenDecryptionResult : ValidationResult + { + private Exception? _exception; + private string? _decryptedToken; + + /// + /// Creates an instance of containing the clear text result of decrypting a security token. + /// + /// The clear text result of decrypting the security token. + /// The SecurityToken that contains the cypher text. + public TokenDecryptionResult(string decryptedToken, SecurityToken securityToken) + : base(ValidationFailureType.ValidationSucceeded) + { + IsValid = true; + _decryptedToken = decryptedToken; + SecurityToken = securityToken; + } + + /// + /// Creates an instance of + /// + /// is the securityToken that could not be decrypted. + /// is the that occurred during reading. + /// is the that occurred during reading. + public TokenDecryptionResult(SecurityToken? securityToken, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + SecurityToken = securityToken; + IsValid = false; + } + + /// + /// Creates an instance of representing a failure due to a null parameter. + /// + /// The securityToken that could not be decrypted. + /// The name of the null parameter. + internal static TokenDecryptionResult NullParameterFailure(SecurityToken? securityToken, string parameterName) => + new TokenDecryptionResult( + securityToken, + ValidationFailureType.TokenDecryptionFailed, + ExceptionDetail.NullParameter(parameterName)); + + /// + /// Gets the decoded contents of the SecurityToken. + /// + /// if the result is not valid, and the decrypted token is not available. + /// It is expected that this method will only be called if returns true. + public string DecryptedToken() + { + if (_decryptedToken is null) + throw new InvalidOperationException("Attempted to retrieve the DecryptedToken from a failed TokenDecrypting result."); + + return _decryptedToken; + } + + /// + /// Gets the that occurred during reading. + /// + public override Exception? Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + + if (_exception is SecurityTokenException securityTokenException) + { + securityTokenException.Source = "Microsoft.IdentityModel.Tokens"; + securityTokenException.ExceptionDetail = ExceptionDetail; + } + + return _exception; + } + } + + /// + /// The on which decryption was attempted. + /// + public SecurityToken? SecurityToken { get; } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index f185d73eac..479d2ff19a 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -75,6 +75,12 @@ private class TokenReplayValidationFailure : ValidationFailureType { internal To public static readonly ValidationFailureType TokenReadingFailed = new TokenReadingFailure("TokenReadingFailed"); private class TokenReadingFailure : ValidationFailureType { internal TokenReadingFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that a JWE could not be decrypted. + /// + public static readonly ValidationFailureType TokenDecryptionFailed = new TokenDecryptionFailure("TokenDecryptionFailed"); + private class TokenDecryptionFailure : ValidationFailureType { internal TokenDecryptionFailure(string name) : base(name) { } } + /// /// Defines a type that represents that no evaluation has taken place. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs index 5977ca0660..c064531798 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationParameters.cs @@ -23,11 +23,12 @@ internal class ValidationParameters private Dictionary _instancePropertyBag; private IList _validTokenTypes = []; + private AlgorithmValidatorDelegate _algorithmValidator = Validators.ValidateAlgorithm; private AudienceValidatorDelegate _audienceValidator = Validators.ValidateAudience; private IssuerValidationDelegateAsync _issuerValidatorAsync = Validators.ValidateIssuerAsync; private LifetimeValidatorDelegate _lifetimeValidator = Validators.ValidateLifetime; - private TypeValidatorDelegate _typeValidator = Validators.ValidateTokenType; private TokenReplayValidatorDelegate _tokenReplayValidator = Validators.ValidateTokenReplay; + private TypeValidatorDelegate _typeValidator = Validators.ValidateTokenType; /// /// This is the default value of when creating a . @@ -109,13 +110,16 @@ public ValidationParameters() public ValidationParameters ActorValidationParameters { get; set; } /// - /// Gets or sets a delegate used to validate the cryptographic algorithm used. + /// Allows overriding the delegate used to validate the cryptographic algorithm used. /// /// - /// If set, this delegate will validate the cryptographic algorithm used and - /// the algorithm will not be checked against . + /// If no delegate is set, the default implementation will be used. The default checks the algorithm + /// against the property, if present. If not, it will succeed. /// - public AlgorithmValidator AlgorithmValidator { get; set; } + public AlgorithmValidatorDelegate AlgorithmValidator { + get { return _algorithmValidator; } + set { _algorithmValidator = value ?? throw new ArgumentNullException(nameof(value), "AlgorithmValidator cannot be null."); } + } /// /// Allows overriding the delegate that will be used to validate the audience. @@ -128,14 +132,8 @@ public ValidationParameters() /// The used to validate the issuer of a token public AudienceValidatorDelegate AudienceValidator { - get - { - return _audienceValidator; - } - set - { - _audienceValidator = value ?? throw new ArgumentNullException(nameof(value), "AudienceValidator cannot be set as null."); - } + get { return _audienceValidator; } + set { _audienceValidator = value ?? throw new ArgumentNullException(nameof(value), "AudienceValidator cannot be set as null."); } } /// @@ -245,6 +243,11 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// public string DebugId { get; set; } + /// + /// Gets the representing the ephemeral decryption key used for decryption by certain algorithms. + /// + public SecurityKey EphemeralDecryptionKey { get; set; } + /// /// Gets or sets a boolean that controls if a '/' is significant at the end of the audience. /// The default is true. @@ -301,14 +304,8 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// The used to validate the issuer of a token public IssuerValidationDelegateAsync IssuerValidatorAsync { - get - { - return _issuerValidatorAsync; - } - set - { - _issuerValidatorAsync = value ?? throw new ArgumentNullException(nameof(value), "IssuerValidatorAsync cannot be set as null."); - } + get { return _issuerValidatorAsync; } + set { _issuerValidatorAsync = value ?? throw new ArgumentNullException(nameof(value), "IssuerValidatorAsync cannot be set as null."); } } /// @@ -323,14 +320,8 @@ public IssuerValidationDelegateAsync IssuerValidatorAsync /// The used to validate the lifetime of a token public LifetimeValidatorDelegate LifetimeValidator { - get - { - return _lifetimeValidator; - } - set - { - _lifetimeValidator = value ?? throw new ArgumentNullException(nameof(value), "LifetimeValidator cannot be set as null."); - } + get { return _lifetimeValidator; } + set { _lifetimeValidator = value ?? throw new ArgumentNullException(nameof(value), "LifetimeValidator cannot be set as null."); } } /// @@ -447,12 +438,12 @@ public string RoleClaimType /// /// This will be used to decrypt the token. This can be helpful when the does not contain a key identifier. /// - public TokenDecryptionKeyResolver TokenDecryptionKeyResolver { get; set; } + public ResolveTokenDecryptionKeyDelegate TokenDecryptionKeyResolver { get; set; } /// /// Gets the that is to be used for decrypting inbound tokens. /// - public IList TokenDecryptionKeys { get; } + public IList TokenDecryptionKeys { get; internal set; } /// /// Gets or set the that store tokens that can be checked to help detect token replay. @@ -471,15 +462,8 @@ public string RoleClaimType /// The used to validate the token replay of the token. public TokenReplayValidatorDelegate TokenReplayValidator { - get - { - return _tokenReplayValidator; - } - - set - { - _tokenReplayValidator = value ?? throw new ArgumentNullException(nameof(value), "TokenReplayValidator cannot be set as null."); - } + get { return _tokenReplayValidator; } + set { _tokenReplayValidator = value ?? throw new ArgumentNullException(nameof(value), "TokenReplayValidator cannot be set as null."); } } /// @@ -496,15 +480,8 @@ public TokenReplayValidatorDelegate TokenReplayValidator /// The used to validate the token type of a token public TypeValidatorDelegate TypeValidator { - get - { - return _typeValidator; - } - - set - { - _typeValidator = value ?? throw new ArgumentNullException(nameof(value), "TypeValidator cannot be set as null."); - } + get { return _typeValidator; } + set { _typeValidator = value ?? throw new ArgumentNullException(nameof(value), "TypeValidator cannot be set as null."); } } /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs index f1ccdaa728..7cfe70b870 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Algorithm.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Microsoft.IdentityModel.Logging; @@ -9,6 +10,23 @@ #nullable enable namespace Microsoft.IdentityModel.Tokens { + /// + /// Definition for delegate that will validate a given algorithm for a . + /// + /// The algorithm to be validated. + /// The that signed the . + /// The being validated. + /// required for validation. + /// + /// A that contains the results of validating the algorithm. + /// This delegate is not expected to throw. + internal delegate AlgorithmValidationResult AlgorithmValidatorDelegate( + string algorithm, + SecurityKey securityKey, + SecurityToken securityToken, + ValidationParameters validationParameters, + CallContext callContext); + public static partial class Validators { /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs new file mode 100644 index 0000000000..3d4b8dac3f --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.DecryptTokenTests.cs @@ -0,0 +1,232 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.IdentityModel.Tokens.Jwt.Tests; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public class JsonWebTokenHandlerDecryptTokenTests + { + [Theory, MemberData(nameof(JsonWebTokenHandlerDecryptTokenTestCases), DisableDiscoveryEnumeration = false)] + public void DecryptToken(TokenDecryptingTheoryData theoryData) + { + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + if (theoryData.Token == null) + { + string tokenString = null; + if (theoryData.SecurityTokenDescriptor != null) + tokenString = jsonWebTokenHandler.CreateToken(theoryData.SecurityTokenDescriptor); + else + tokenString = theoryData.TokenString; + + if (tokenString != null) + theoryData.Token = new JsonWebToken(tokenString); + } + + if (theoryData.TestId == "Invalid_NoKeysProvided") + { +#pragma warning disable CS0219 // Variable is assigned but its value is never used + var something = 0; +#pragma warning restore CS0219 // Variable is assigned but its value is never used + } + + CompareContext context = TestUtilities.WriteHeader($"{this}.JsonWebTokenHandlerDecryptTokenTests", theoryData); + TokenDecryptionResult tokenDecryptionResult = jsonWebTokenHandler.DecryptToken( + theoryData.Token, + theoryData.ValidationParameters, + theoryData.Configuration, + new CallContext()); + + if (tokenDecryptionResult.Exception != null) + theoryData.ExpectedException.ProcessException(tokenDecryptionResult.Exception); + else + theoryData.ExpectedException.ProcessNoException(); + + IdentityComparer.AreTokenDecryptingResultsEqual( + tokenDecryptionResult, + theoryData.TokenDecryptionResult, + context); + + TestUtilities.AssertFailIfErrors(context); + } + + [Fact] + public void DecryptToken_ThrowsIfAccessingSecurityTokenOnFailedRead() + { + JsonWebTokenHandler jsonWebTokenHandler = new JsonWebTokenHandler(); + TokenDecryptionResult tokenDecryptionResult = jsonWebTokenHandler.DecryptToken( + null, + null, + null, + new CallContext()); + + Assert.Throws(() => tokenDecryptionResult.DecryptedToken()); + } + + public static TheoryData JsonWebTokenHandlerDecryptTokenTestCases + { + get + { + var validToken = EncodedJwts.LiveJwt; + var token = new JsonWebToken(validToken); +#if NET472 || NET6_0_OR_GREATER + var ecdsaEncryptingCredentials = new EncryptingCredentials( + new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, true), + SecurityAlgorithms.EcdhEsA256kw, + SecurityAlgorithms.Aes128CbcHmacSha256) + { + KeyExchangePublicKey = KeyingMaterial.JsonWebKeyP256_Public + }; + var ecdsaTokenDescriptor = new SecurityTokenDescriptor + { + EncryptingCredentials = ecdsaEncryptingCredentials, + Expires = DateTime.MaxValue, + NotBefore = DateTime.MinValue, + IssuedAt = DateTime.MinValue, + }; + + var jsonWebTokenHandler = new JsonWebTokenHandler(); + var ecdsaToken = new JsonWebToken(jsonWebTokenHandler.CreateToken(ecdsaTokenDescriptor)); +#endif + + return new TheoryData + { + new TokenDecryptingTheoryData + { + TestId = "Invalid_TokenIsNotEncrypted", + Token = token, + ValidationParameters = new ValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenException("IDX10612:"), + TokenDecryptionResult = new TokenDecryptionResult( + token, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10612), + typeof(SecurityTokenException), + new StackFrame(), null)), + }, + new TokenDecryptingTheoryData + { + TestId = "Invalid_SecurityTokenIsNull", + Token = null, + ValidationParameters = new ValidationParameters(), + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + TokenDecryptionResult = new TokenDecryptionResult( + null, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10000, "jwtToken"), + typeof(ArgumentNullException), + new StackFrame(true))), + }, + new TokenDecryptingTheoryData + { + TestId = "Invalid_ValidationParametersIsNull", + Token = token, + ValidationParameters = null, + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + TokenDecryptionResult = new TokenDecryptionResult( + token, + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail(TokenLogMessages.IDX10000, "validationParameters"), + typeof(ArgumentNullException), + new StackFrame(true))), + }, + new TokenDecryptingTheoryData + { + TestId = "Valid_Aes128_FromValidationParameters", + TokenString = ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims, + ValidationParameters = new ValidationParameters + { + TokenDecryptionKeys = [Default.SymmetricEncryptingCredentials.Key], + }, + TokenDecryptionResult = new TokenDecryptionResult( + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6IkJvYkBjb250b3NvLmNvbSIsImdpdmVuX25hbWUiOiJCb2IiLCJpc3MiOiJodHRwOi8vRGVmYXVsdC5Jc3N1ZXIuY29tIiwiYXVkIjoiaHR0cDovL0RlZmF1bHQuQXVkaWVuY2UuY29tIiwiaWF0IjoiMTQ4OTc3NTYxNyIsIm5iZiI6IjE0ODk3NzU2MTciLCJleHAiOiIyNTM0MDIzMDA3OTkifQ.", + new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims)), + }, + new TokenDecryptingTheoryData + { + TestId = "Valid_Aes128_FromKeyResolver", + TokenString = ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims, + ValidationParameters = new ValidationParameters + { + TokenDecryptionKeyResolver = (tokenString, token, kid, validationParameters, callContext) => [Default.SymmetricEncryptingCredentials.Key] + }, + TokenDecryptionResult = new TokenDecryptionResult( + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6IkJvYkBjb250b3NvLmNvbSIsImdpdmVuX25hbWUiOiJCb2IiLCJpc3MiOiJodHRwOi8vRGVmYXVsdC5Jc3N1ZXIuY29tIiwiYXVkIjoiaHR0cDovL0RlZmF1bHQuQXVkaWVuY2UuY29tIiwiaWF0IjoiMTQ4OTc3NTYxNyIsIm5iZiI6IjE0ODk3NzU2MTciLCJleHAiOiIyNTM0MDIzMDA3OTkifQ.", + new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims)), + }, + new TokenDecryptingTheoryData + { + TestId = "Valid_Aes128_FromConfiguration", + TokenString = ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims, + ValidationParameters = new ValidationParameters(), + Configuration = new CustomConfiguration(Default.SymmetricEncryptingCredentials.Key), + TokenDecryptionResult = new TokenDecryptionResult( + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJlbWFpbCI6IkJvYkBjb250b3NvLmNvbSIsImdpdmVuX25hbWUiOiJCb2IiLCJpc3MiOiJodHRwOi8vRGVmYXVsdC5Jc3N1ZXIuY29tIiwiYXVkIjoiaHR0cDovL0RlZmF1bHQuQXVkaWVuY2UuY29tIiwiaWF0IjoiMTQ4OTc3NTYxNyIsIm5iZiI6IjE0ODk3NzU2MTciLCJleHAiOiIyNTM0MDIzMDA3OTkifQ.", + new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims)), + }, +#if NET472 || NET6_0_OR_GREATER + new TokenDecryptingTheoryData + { + TestId = "Valid_Ecdsa256_FromValidationParameters", + Token = ecdsaToken, + ValidationParameters = new ValidationParameters + { + TokenDecryptionKeys = [new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, true)], + EphemeralDecryptionKey = new ECDsaSecurityKey(KeyingMaterial.JsonWebKeyP256, true) + }, + TokenDecryptionResult = new TokenDecryptionResult( + "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJleHAiOjI1MzQwMjMwMDgwMCwiaWF0IjowLCJuYmYiOjB9.", + ecdsaToken), + }, +#endif + new TokenDecryptingTheoryData + { + TestId = "Invalid_NoKeysProvided", + TokenString = ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims, + ValidationParameters = new ValidationParameters(), + ExpectedException = ExpectedException.SecurityTokenDecryptionFailedException("IDX10609:"), + TokenDecryptionResult = new TokenDecryptionResult( + new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims), + ValidationFailureType.TokenDecryptionFailed, + new ExceptionDetail( + new MessageDetail( + TokenLogMessages.IDX10609, + LogHelper.MarkAsSecurityArtifact( + new JsonWebToken(ReferenceTokens.JWEDirectEncryptionUnsignedInnerJWTWithAdditionalHeaderClaims), + JwtTokenUtilities.SafeLogJwtToken)), + typeof(SecurityTokenDecryptionFailedException), + new StackFrame(), null)), + } + }; + } + } + } + + public class TokenDecryptingTheoryData : TheoryDataBase + { + public JsonWebToken Token { get; set; } + internal TokenDecryptionResult TokenDecryptionResult { get; set; } + public BaseConfiguration Configuration { get; internal set; } + public SecurityTokenDescriptor SecurityTokenDescriptor { get; internal set; } + public string TokenString { get; internal set; } + internal ValidationParameters ValidationParameters { get; set; } + } + + public class CustomConfiguration : BaseConfiguration + { + public CustomConfiguration(SecurityKey tokenDecryptionKey) : base() + { + TokenDecryptionKeys.Add(tokenDecryptionKey); + } + } +} diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 501275d644..0e8a3ae226 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -756,7 +756,7 @@ public static bool AreSigningKeyValidationResultsEqual(object object1, object ob null, context); } - + internal static bool AreSigningKeyValidationResultsEqual( SigningKeyValidationResult signingKeyValidationResult1, SigningKeyValidationResult signingKeyValidationResult2, @@ -803,10 +803,10 @@ internal static bool AreSigningKeyValidationResultsEqual( stackPrefix.Trim(), localContext); } - + return context.Merge(localContext); } - + public static bool AreLifetimeValidationResultsEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); @@ -1047,7 +1047,7 @@ internal static bool AreTokenReadingResultsEqual( if (tokenReadingResult1.ValidationFailureType != tokenReadingResult2.ValidationFailureType) localContext.Diffs.Add($"TokenReadingResult1.ValidationFailureType: {tokenReadingResult1.ValidationFailureType} != TokenReadingResult2.ValidationFailureType: {tokenReadingResult2.ValidationFailureType}"); - + // true => both are not null. if (ContinueCheckingEquality(tokenReadingResult1.Exception, tokenReadingResult2.Exception, localContext)) { @@ -1078,6 +1078,81 @@ internal static bool AreTokenReadingResultsEqual( return context.Merge(localContext); } + public static bool AreTokenDecryptingResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreTokenDecryptingResultsEqual( + object1 as TokenDecryptionResult, + object2 as TokenDecryptionResult, + "TokenDecryptingResult1", + "TokenDecryptingResult2", + null, + context); + } + + internal static bool AreTokenDecryptingResultsEqual( + TokenDecryptionResult tokenDecryptingResult1, + TokenDecryptionResult tokenDecryptingResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(tokenDecryptingResult1, tokenDecryptingResult2, localContext)) + return context.Merge(localContext); + + if (tokenDecryptingResult1.IsValid != tokenDecryptingResult2.IsValid) + localContext.Diffs.Add($"TokenDecryptingResult1.IsValid: {tokenDecryptingResult1.IsValid} != TokenDecryptingResult2.IsValid: {tokenDecryptingResult2.IsValid}"); + + if (tokenDecryptingResult1.SecurityToken == null || tokenDecryptingResult2.SecurityToken == null) + { + if (tokenDecryptingResult1.SecurityToken != tokenDecryptingResult2.SecurityToken) + localContext.Diffs.Add($"TokenDecryptingResult1.SecurityToken: '{tokenDecryptingResult1.SecurityToken}' != TokenDecryptingResult2.SecurityToken: '{tokenDecryptingResult2.SecurityToken}'"); + } + else if (tokenDecryptingResult1.SecurityToken.ToString() != tokenDecryptingResult2.SecurityToken.ToString()) + localContext.Diffs.Add($"TokenDecryptingResult1.SecurityToken: '{tokenDecryptingResult1.SecurityToken}' != TokenDecryptingResult2.SecurityToken: '{tokenDecryptingResult2.SecurityToken}'"); + + // Only compare the decrypted token if both results are valid. + if (tokenDecryptingResult1.IsValid && (tokenDecryptingResult1.DecryptedToken().ToString() != tokenDecryptingResult2.DecryptedToken().ToString())) + localContext.Diffs.Add($"TokenDecryptingResult1.DecryptedToken: '{tokenDecryptingResult1.DecryptedToken()}' != TokenDecryptingResult2.DecryptedToken: '{tokenDecryptingResult2.DecryptedToken()}'"); + + if (tokenDecryptingResult1.ValidationFailureType != tokenDecryptingResult2.ValidationFailureType) + localContext.Diffs.Add($"TokenDecryptingResult1.ValidationFailureType: {tokenDecryptingResult1.ValidationFailureType} != TokenDecryptingResult1.ValidationFailureType: {tokenDecryptingResult2.ValidationFailureType}"); + + // true => both are not null. + if (ContinueCheckingEquality(tokenDecryptingResult1.Exception, tokenDecryptingResult2.Exception, localContext)) + { + AreStringsEqual( + tokenDecryptingResult1.Exception.Message, + tokenDecryptingResult2.Exception.Message, + $"({name1}).Exception.Message", + $"({name2}).Exception.Message", + localContext); + + AreStringsEqual( + tokenDecryptingResult1.Exception.Source, + tokenDecryptingResult2.Exception.Source, + $"({name1}).Exception.Source", + $"({name2}).Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + tokenDecryptingResult1.Exception.StackTrace.Trim(), + tokenDecryptingResult2.Exception.StackTrace.Trim(), + $"({name1}).Exception.StackTrace", + $"({name2}).Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context);