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);