From f27016f905a543c2935259005b705c4a0ccf4505 Mon Sep 17 00:00:00 2001
From: RojaEnnam <63254595+RojaEnnam@users.noreply.github.com>
Date: Thu, 30 Mar 2023 17:12:40 -0700
Subject: [PATCH] Add encryption keys to base configuration (#2023)
* Add encryption keys
* Fix tests
* Use config for JWE
* Fix test
* Add tests
* Address comments
* Add JsonIgnore in BaseConfiguration
---
.../GlobalSuppressions.cs | 2 +-
.../JsonWebTokenHandler.cs | 62 +++++++++++++++++--
.../BaseConfiguration.cs | 10 +++
.../LogMessages.cs | 2 +
.../JsonWebTokenHandlerTests.cs | 54 +++++++++++++++-
.../OpenIdConnectConfigurationTests.cs | 2 +-
6 files changed, 125 insertions(+), 7 deletions(-)
diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
index e004b3693e..ee09ac57fc 100644
--- a/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/GlobalSuppressions.cs
@@ -20,7 +20,6 @@
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")]
[assembly: SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Has no fields or properties", Scope = "type", Target = "~T:Microsoft.IdentityModel.JsonWebTokens.JwtHeaderParameterNames")]
[assembly: SuppressMessage("Performance", "CA1815:Override equals and operator equals on value types", Justification = "Has no fields or properties", Scope = "type", Target = "~T:Microsoft.IdentityModel.JsonWebTokens.JwtRegisteredClaimNames")]
-[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.GetContentEncryptionKeys(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters)~System.Collections.Generic.IEnumerable{Microsoft.IdentityModel.Tokens.SecurityKey}")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JwtTokenUtilities.DecryptJwtToken(Microsoft.IdentityModel.Tokens.SecurityToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.JsonWebTokens.JwtTokenDecryptionParameters)~System.String")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(System.String,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is returned in the TokenValidationResult", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateJWE(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,System.String,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.Tokens.TokenValidationResult")]
@@ -32,3 +31,4 @@
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.CreateTokenPrivate(System.String,Microsoft.IdentityModel.Tokens.SigningCredentials,Microsoft.IdentityModel.Tokens.EncryptingCredentials,System.String,System.Collections.Generic.IDictionary{System.String,System.Object},System.Collections.Generic.IDictionary{System.String,System.Object},System.String)~System.String")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(System.String,Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")]
[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "There are additional keys to check, the next one may be successful", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateSignature(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~Microsoft.IdentityModel.JsonWebTokens.JsonWebToken")]
+[assembly: SuppressMessage("Design", "CA1031:Do not catch general exception types", Justification = "Exception is written to a string", Scope = "member", Target = "~M:Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.GetContentEncryptionKeys(Microsoft.IdentityModel.JsonWebTokens.JsonWebToken,Microsoft.IdentityModel.Tokens.TokenValidationParameters,Microsoft.IdentityModel.Tokens.BaseConfiguration)~System.Collections.Generic.IEnumerable{Microsoft.IdentityModel.Tokens.SecurityKey}")]
diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
index 49b4c84ebf..92cf3ea839 100644
--- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
@@ -753,6 +753,11 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV
/// if ' .Kid' is not null AND decryption fails.
/// if the JWE was not able to be decrypted.
public string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters validationParameters)
+ {
+ return DecryptToken(jwtToken, validationParameters, null);
+ }
+
+ private string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
{
if (jwtToken == null)
throw LogHelper.LogArgumentNullException(nameof(jwtToken));
@@ -763,7 +768,7 @@ public string DecryptToken(JsonWebToken jwtToken, TokenValidationParameters vali
if (string.IsNullOrEmpty(jwtToken.Enc))
throw LogHelper.LogExceptionMessage(new SecurityTokenException(LogHelper.FormatInvariant(TokenLogMessages.IDX10612)));
- var keys = GetContentEncryptionKeys(jwtToken, validationParameters);
+ var keys = GetContentEncryptionKeys(jwtToken, validationParameters, configuration);
return JwtTokenUtilities.DecryptJwtToken(
jwtToken,
validationParameters,
@@ -939,10 +944,43 @@ private static string EncryptTokenPrivate(string innerJwt, EncryptingCredentials
}
}
- internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken, TokenValidationParameters validationParameters)
+ private static SecurityKey ResolveTokenDecryptionKeyFromConfig(JsonWebToken jwtToken, BaseConfiguration configuration)
+ {
+ if (jwtToken == null)
+ throw LogHelper.LogArgumentNullException(nameof(jwtToken));
+
+ if (!string.IsNullOrEmpty(jwtToken.Kid) && configuration.TokenDecryptionKeys != null)
+ {
+ foreach (var key in configuration.TokenDecryptionKeys)
+ {
+ if (key != null && string.Equals(key.KeyId, jwtToken.Kid, GetStringComparisonRuleIf509OrECDsa(key)))
+ return key;
+ }
+ }
+
+ if (!string.IsNullOrEmpty(jwtToken.X5t) && configuration.TokenDecryptionKeys != null)
+ {
+ foreach (var key in configuration.TokenDecryptionKeys)
+ {
+ 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;
+ }
+
+ internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
{
IEnumerable keys = null;
+ // First we check to see if the caller has set a custom decryption resolver on TVP for the call, if so any keys set on TVP and keys in Configuration are ignored.
+ // If no custom decryption resolver set, we'll check to see if they've set some static decryption keys on TVP. If a key found, we ignore configuration.
+ // If no key found in TVP, we'll check the configuration.
if (validationParameters.TokenDecryptionKeyResolver != null)
{
keys = validationParameters.TokenDecryptionKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters);
@@ -951,7 +989,18 @@ internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken
{
var key = ResolveTokenDecryptionKey(jwtToken.EncodedToken, jwtToken, validationParameters);
if (key != null)
- keys = new List { key };
+ {
+ LogHelper.LogInformation(TokenLogMessages.IDX10904, key);
+ }
+ else if (configuration != null)
+ {
+ key = ResolveTokenDecryptionKeyFromConfig(jwtToken, configuration);
+ if ( key != null )
+ LogHelper.LogInformation(TokenLogMessages.IDX10905, key);
+ }
+
+ if (key != null)
+ keys = new List { 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
@@ -960,9 +1009,14 @@ internal IEnumerable GetContentEncryptionKeys(JsonWebToken jwtToken
// 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 = JwtTokenUtilities.GetAllDecryptionKeys(validationParameters);
+ if (configuration != null)
+ keys = keys == null ? configuration.TokenDecryptionKeys : keys.Concat(configuration.TokenDecryptionKeys);
+ }
if (jwtToken.Alg.Equals(JwtConstants.DirectKeyUseAlg, StringComparison.Ordinal)
|| jwtToken.Alg.Equals(SecurityAlgorithms.EcdhEs, StringComparison.Ordinal))
@@ -1335,7 +1389,7 @@ private TokenValidationResult ValidateJWE(JsonWebToken jwtToken, TokenValidation
{
try
{
- string jws = DecryptToken(jwtToken, validationParameters);
+ string jws = DecryptToken(jwtToken, validationParameters, configuration);
TokenValidationResult readTokenResult = ReadToken(jws, validationParameters);
if (!readTokenResult.IsValid)
return readTokenResult;
diff --git a/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs b/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs
index bc31faead6..bebbcbb95d 100644
--- a/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs
+++ b/src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs
@@ -3,6 +3,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
+using Microsoft.IdentityModel.Json;
namespace Microsoft.IdentityModel.Tokens
{
@@ -30,5 +31,14 @@ public virtual ICollection SigningKeys
/// Or the token_endpoint in the OIDC metadata.
///
public virtual string TokenEndpoint { get; set; }
+
+ ///
+ /// Gets the that the IdentityProvider indicates are to be used in order to decrypt tokens.
+ ///
+ [JsonIgnore]
+ public virtual ICollection TokenDecryptionKeys
+ {
+ get;
+ } = new Collection();
}
}
diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
index d5a720d9e8..5c3084dd0c 100644
--- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
+++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs
@@ -120,6 +120,8 @@ internal static class LogMessages
public const string IDX10619 = "IDX10619: Decryption failed. Algorithm: '{0}'. Either the Encryption Algorithm: '{1}' or none of the Security Keys are supported by the CryptoProviderFactory.";
public const string IDX10620 = "IDX10620: Unable to obtain a CryptoProviderFactory, both EncryptingCredentials.CryptoProviderFactory and EncryptingCredentials.Key.CrypoProviderFactory are null.";
//public const string IDX10903 = "IDX10903: Token decryption succeeded. With thumbprint: '{0}'.";
+ public const string IDX10904 = "IDX10904: Token decryption key : '{0}' found in TokenValidationParameters.";
+ public const string IDX10905 = "IDX10905: Token decryption key : '{0}' found in Configuration/Metadata.";
// Formating
public const string IDX10400 = "IDX10400: Unable to decode: '{0}' as Base64url encoded string.";
diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
index bba60f627a..7cf88fc6d7 100644
--- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
+++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs
@@ -631,7 +631,7 @@ public void GetEncryptionKeys(CreateTokenTheoryData theoryData)
{
string jweFromJsonHandlerWithKid = theoryData.JsonWebTokenHandler.CreateToken(theoryData.Payload, theoryData.TokenDescriptor.SigningCredentials, theoryData.TokenDescriptor.EncryptingCredentials);
var jwtTokenFromJsonHandlerWithKid = new JsonWebToken(jweFromJsonHandlerWithKid);
- var encryptionKeysFromJsonHandlerWithKid = theoryData.JsonWebTokenHandler.GetContentEncryptionKeys(jwtTokenFromJsonHandlerWithKid, theoryData.ValidationParameters);
+ var encryptionKeysFromJsonHandlerWithKid = theoryData.JsonWebTokenHandler.GetContentEncryptionKeys(jwtTokenFromJsonHandlerWithKid, theoryData.ValidationParameters, theoryData.Configuration);
IdentityComparer.AreEqual(encryptionKeysFromJsonHandlerWithKid, theoryData.ExpectedDecryptionKeys);
theoryData.ExpectedException.ProcessNoException(context);
@@ -643,6 +643,7 @@ public void GetEncryptionKeys(CreateTokenTheoryData theoryData)
TestUtilities.AssertFailIfErrors(context);
}
+
public static TheoryData SecurityTokenDecryptionTheoryData
{
get
@@ -652,12 +653,61 @@ public static TheoryData SecurityTokenDecryptionTheoryDat
SetDefaultTimesOnTokenCreation = false
};
+ var configurationWithDecryptionKeys = new OpenIdConnectConfiguration();
+ configurationWithDecryptionKeys.TokenDecryptionKeys.Add(KeyingMaterial.DefaultSymmetricSecurityKey_256);
+ configurationWithDecryptionKeys.TokenDecryptionKeys.Add(KeyingMaterial.DefaultSymmetricSecurityKey_512);
+
tokenHandler.InboundClaimTypeMap.Clear();
return new TheoryData
{
new CreateTokenTheoryData
{
First = true,
+ TestId = "EncryptionKeyInConfig",
+ Payload = Default.PayloadString,
+ TokenDescriptor = new SecurityTokenDescriptor
+ {
+ SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
+ EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2,
+ Claims = Default.PayloadDictionary
+ },
+ JsonWebTokenHandler = new JsonWebTokenHandler(),
+ ValidationParameters = new TokenValidationParameters
+ {
+ IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key,
+ ValidAudience = Default.Audience,
+ ValidIssuer = Default.Issuer
+ },
+ Configuration = configurationWithDecryptionKeys,
+ ExpectedDecryptionKeys = new List(){ KeyingMaterial.DefaultSymmetricSecurityKey_256 },
+ Algorithm = JwtConstants.DirectKeyUseAlg,
+ EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2_NoKeyId
+ },
+ new CreateTokenTheoryData
+ {
+ TestId = "ValidEncryptionKeyInConfig",
+ Payload = Default.PayloadString,
+ TokenDescriptor = new SecurityTokenDescriptor
+ {
+ SigningCredentials = KeyingMaterial.JsonWebKeyRsa256SigningCredentials,
+ EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2,
+ Claims = Default.PayloadDictionary
+ },
+ JsonWebTokenHandler = new JsonWebTokenHandler(),
+ ValidationParameters = new TokenValidationParameters
+ {
+ IssuerSigningKey = KeyingMaterial.JsonWebKeyRsa256SigningCredentials.Key,
+ TokenDecryptionKeys = new List(){ KeyingMaterial.DefaultSymmetricSecurityKey_512 },
+ ValidAudience = Default.Audience,
+ ValidIssuer = Default.Issuer
+ },
+ Configuration = configurationWithDecryptionKeys,
+ ExpectedDecryptionKeys = new List(){ KeyingMaterial.DefaultSymmetricSecurityKey_256 },
+ Algorithm = JwtConstants.DirectKeyUseAlg,
+ EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2_NoKeyId
+ },
+ new CreateTokenTheoryData
+ {
TestId = "Valid",
Payload = Default.PayloadString,
TokenDescriptor = new SecurityTokenDescriptor
@@ -3449,6 +3499,8 @@ public CreateTokenTheoryData(string testId)
public string CompressionAlgorithm { get; set; }
+ public BaseConfiguration Configuration { get; set; }
+
public CompressionProviderFactory CompressionProviderFactory { get; set; }
public EncryptingCredentials EncryptingCredentials { get; set; }
diff --git a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs
index cd49f5204b..98cdd75625 100644
--- a/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs
+++ b/test/Microsoft.IdentityModel.Protocols.OpenIdConnect.Tests/OpenIdConnectConfigurationTests.cs
@@ -108,7 +108,7 @@ public void GetSets()
OpenIdConnectConfiguration configuration = new OpenIdConnectConfiguration();
Type type = typeof(OpenIdConnectConfiguration);
PropertyInfo[] properties = type.GetProperties();
- if (properties.Length != 47)
+ if (properties.Length != 48)
Assert.True(false, "Number of properties has changed from 47 to: " + properties.Length + ", adjust tests");
TestUtilities.CallAllPublicInstanceAndStaticPropertyGets(configuration, "OpenIdConnectConfiguration_GetSets");