Skip to content

Commit

Permalink
Add encryption keys to base configuration (#2023)
Browse files Browse the repository at this point in the history
* Add encryption keys

* Fix tests

* Use config for JWE

* Fix test

* Add tests

* Address comments

* Add JsonIgnore in BaseConfiguration
  • Loading branch information
RojaEnnam authored Mar 31, 2023
1 parent 3696bd2 commit f27016f
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 7 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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}")]
62 changes: 58 additions & 4 deletions src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,11 @@ private ClaimsIdentity CreateClaimsIdentityPrivate(JsonWebToken jwtToken, TokenV
/// <exception cref="SecurityTokenEncryptionKeyNotFoundException">if '<paramref name="jwtToken"/> .Kid' is not null AND decryption fails.</exception>
/// <exception cref="SecurityTokenDecryptionFailedException">if the JWE was not able to be decrypted.</exception>
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));
Expand All @@ -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,
Expand Down Expand Up @@ -939,10 +944,43 @@ private static string EncryptTokenPrivate(string innerJwt, EncryptingCredentials
}
}

internal IEnumerable<SecurityKey> 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<SecurityKey> GetContentEncryptionKeys(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration)
{
IEnumerable<SecurityKey> 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);
Expand All @@ -951,7 +989,18 @@ internal IEnumerable<SecurityKey> GetContentEncryptionKeys(JsonWebToken jwtToken
{
var key = ResolveTokenDecryptionKey(jwtToken.EncodedToken, jwtToken, validationParameters);
if (key != null)
keys = new List<SecurityKey> { 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<SecurityKey> { 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
Expand All @@ -960,9 +1009,14 @@ internal IEnumerable<SecurityKey> 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))
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/BaseConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Collections.Generic;
using System.Collections.ObjectModel;
using Microsoft.IdentityModel.Json;

namespace Microsoft.IdentityModel.Tokens
{
Expand Down Expand Up @@ -30,5 +31,14 @@ public virtual ICollection<SecurityKey> SigningKeys
/// Or the token_endpoint in the OIDC metadata.
/// </summary>
public virtual string TokenEndpoint { get; set; }

/// <summary>
/// Gets the <see cref="ICollection{SecurityKey}"/> that the IdentityProvider indicates are to be used in order to decrypt tokens.
/// </summary>
[JsonIgnore]
public virtual ICollection<SecurityKey> TokenDecryptionKeys
{
get;
} = new Collection<SecurityKey>();
}
}
2 changes: 2 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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.";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -643,6 +643,7 @@ public void GetEncryptionKeys(CreateTokenTheoryData theoryData)

TestUtilities.AssertFailIfErrors(context);
}

public static TheoryData<CreateTokenTheoryData> SecurityTokenDecryptionTheoryData
{
get
Expand All @@ -652,12 +653,61 @@ public static TheoryData<CreateTokenTheoryData> 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<CreateTokenTheoryData>
{
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<SecurityKey>(){ 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<SecurityKey>(){ KeyingMaterial.DefaultSymmetricSecurityKey_512 },
ValidAudience = Default.Audience,
ValidIssuer = Default.Issuer
},
Configuration = configurationWithDecryptionKeys,
ExpectedDecryptionKeys = new List<SecurityKey>(){ KeyingMaterial.DefaultSymmetricSecurityKey_256 },
Algorithm = JwtConstants.DirectKeyUseAlg,
EncryptingCredentials = KeyingMaterial.DefaultSymmetricEncryptingCreds_Aes128_Sha2_NoKeyId
},
new CreateTokenTheoryData
{
TestId = "Valid",
Payload = Default.PayloadString,
TokenDescriptor = new SecurityTokenDescriptor
Expand Down Expand Up @@ -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; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down

0 comments on commit f27016f

Please sign in to comment.