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