Skip to content

Commit

Permalink
Resolve a signing key using both KeyId and X5t in a consistent manner (
Browse files Browse the repository at this point in the history
…#2061)

* Resolve a signing key using both KeyId and X5t in a consistent manner

* Removed ResolveTokenSigningKeyUsingConfiguration() and ResolveTokenSigningKeyUsingValidationParameters() and updated the test

* Skip a flaky test
  • Loading branch information
dannybtsai committed Apr 26, 2023
1 parent 3db0e02 commit 2a1cf33
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 55 deletions.
83 changes: 30 additions & 53 deletions src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
Expand Down Expand Up @@ -438,79 +439,40 @@ private static long ParseTimeValue(JToken jToken, string claimName)
/// <param name="validationParameters">A <see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="configuration">The <see cref="BaseConfiguration"/> that will be used along with the <see cref="TokenValidationParameters"/> to resolve the signing key</param>
/// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns>
/// <remarks>If key fails to resolve, then null is returned</remarks>
/// <remarks>Resolve the signing key using configuration then the validationParameters until a key is resolved. If key fails to resolve, then null is returned.</remarks>
internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, TokenValidationParameters validationParameters, BaseConfiguration configuration)
{
if (configuration?.SigningKeys != null)
{

if (!string.IsNullOrEmpty(kid))
{
foreach (SecurityKey signingKey in configuration.SigningKeys)
{
if (signingKey != null && string.Equals(signingKey.KeyId, kid, signingKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
return signingKey;
}
}

if (!string.IsNullOrEmpty(x5t))
{
foreach (SecurityKey signingKey in configuration.SigningKeys)
{
if (signingKey != null && string.Equals(signingKey.KeyId, x5t))
return signingKey;
}
}
}

return ResolveTokenSigningKey(kid, x5t, validationParameters);
return ResolveTokenSigningKey(kid, x5t, configuration?.SigningKeys) ?? ResolveTokenSigningKey(kid, x5t, ConcatSigningKeys(validationParameters));
}

/// <summary>
/// Returns a <see cref="SecurityKey"/> to use when validating the signature of a token.
/// </summary>
/// <param name="kid">The <see cref="string"/> kid field of the token being validated</param>
/// <param name="x5t">The <see cref="string"/> x5t field of the token being validated</param>
/// <param name="validationParameters">A <see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="signingKeys">A collection of <see cref="SecurityKey"/> a signing key to be resolved from.</param>
/// <returns>Returns a <see cref="SecurityKey"/> to use for signature validation.</returns>
/// <remarks>If key fails to resolve, then null is returned</remarks>
internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, TokenValidationParameters validationParameters)
internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, IEnumerable<SecurityKey> signingKeys)
{
if (!string.IsNullOrEmpty(kid))
{
if (validationParameters.IssuerSigningKey != null
&& string.Equals(validationParameters.IssuerSigningKey.KeyId, kid, validationParameters.IssuerSigningKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
return validationParameters.IssuerSigningKey;
if (signingKeys == null)
return null;

if (validationParameters.IssuerSigningKeys != null)
foreach (SecurityKey signingKey in signingKeys)
{
if (signingKey != null)
{
foreach (SecurityKey signingKey in validationParameters.IssuerSigningKeys)
if (signingKey is X509SecurityKey x509Key)
{
if (signingKey != null && string.Equals(signingKey.KeyId, kid, signingKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
if ((!string.IsNullOrEmpty(kid) && string.Equals(signingKey.KeyId, kid, StringComparison.OrdinalIgnoreCase)) ||
(!string.IsNullOrEmpty(x5t) && string.Equals(x509Key.X5t, x5t, StringComparison.OrdinalIgnoreCase)))
{
return signingKey;
}
}
}
}

if (!string.IsNullOrEmpty(x5t))
{
if (validationParameters.IssuerSigningKey != null)
{
if (string.Equals(validationParameters.IssuerSigningKey.KeyId, x5t, validationParameters.IssuerSigningKey is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal))
return validationParameters.IssuerSigningKey;

X509SecurityKey x509Key = validationParameters.IssuerSigningKey as X509SecurityKey;
if (x509Key != null && string.Equals(x509Key.X5t, x5t, StringComparison.OrdinalIgnoreCase))
return validationParameters.IssuerSigningKey;
}

if (validationParameters.IssuerSigningKeys != null)
{
foreach (SecurityKey signingKey in validationParameters.IssuerSigningKeys)
else if (!string.IsNullOrEmpty(signingKey.KeyId))
{
if (signingKey != null && string.Equals(signingKey.KeyId, x5t))
if (string.Equals(signingKey.KeyId, kid) || string.Equals(signingKey.KeyId, x5t))
{
return signingKey;
}
Expand All @@ -521,6 +483,21 @@ internal static SecurityKey ResolveTokenSigningKey(string kid, string x5t, Token
return null;
}

internal static IEnumerable<SecurityKey> ConcatSigningKeys(TokenValidationParameters tvp)
{
if (tvp == null)
yield break;

yield return tvp.IssuerSigningKey;
if (tvp.IssuerSigningKeys != null)
{
foreach (var key in tvp.IssuerSigningKeys)
{
yield return key;
}
}
}

#if !NET45
internal static JsonDocument ParseDocument(byte[] bytes, int length)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1645,7 +1645,7 @@ protected virtual SecurityKey ResolveIssuerSigningKey(string token, JwtSecurityT
if (jwtToken == null)
throw LogHelper.LogArgumentNullException(nameof(jwtToken));

return JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Header.Kid, jwtToken.Header.X5t, validationParameters);
return JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Header.Kid, jwtToken.Header.X5t, validationParameters, null);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public AotCompatibilityTests(ITestOutputHelper testOutputHelper)
///
/// You can also 'dotnet publish' the 'Microsoft.IdentityModel.AotCompatibility.TestApp.csproj' as well to get the errors.
/// </summary>
[Fact]
[Fact(Skip = "need to adjust timeout")]
public void EnsureAotCompatibility()
{
string testAppPath = @"..\..\..\..\Microsoft.IdentityModel.AotCompatibility.TestApp";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.TestUtils;
using Microsoft.IdentityModel.Tokens;
using Xunit;

namespace Microsoft.IdentityModel.JsonWebTokens.Tests
{
public class JwtTokenUtilitiesTests
{
[Fact]
public void ResolveTokenSigningKey()
{
var testKeyId = Guid.NewGuid().ToString();
var tvp = new TokenValidationParameters();

// null configuration
var resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, null, tvp, null);
Assert.Null(resolvedKey);

// null tvp
resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, null, null, null);
Assert.Null(resolvedKey);

var signingKey = new X509SecurityKey(KeyingMaterial.CertSelfSigned1024_SHA256);
signingKey.KeyId = testKeyId;
tvp.IssuerSigningKey = signingKey;

#region KeyId

// signingKey.KeyId matches TVP.IssuerSigningKey
resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, null, tvp, null);
Assert.NotNull(resolvedKey);
Assert.Same(resolvedKey, tvp.IssuerSigningKey);

// signingKey.KeyId matched, TVP.IssuerSigningKeys
tvp.IssuerSigningKey = null;
tvp.IssuerSigningKeys = new List<SecurityKey>() { signingKey };

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, Base64UrlEncoder.Encode(testKeyId), tvp, null);
Assert.NotNull(resolvedKey);
Assert.Same(resolvedKey, tvp.IssuerSigningKeys.First());

// signingKey.KeyId matches configuration.SigningKeys.First()
tvp.IssuerSigningKey = null;
tvp.IssuerSigningKeys = null;
var configuration = GetConfigurationMock();
var testSigningKey = configuration.SigningKeys.First();

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testSigningKey.KeyId, string.Empty, tvp, configuration);
Assert.Same(resolvedKey, testSigningKey);

#endregion

#region X5t

// signingKey.X5t matches TVP.IssuerSigningKey
signingKey.KeyId = Guid.NewGuid().ToString();
tvp.IssuerSigningKey = signingKey;
tvp.IssuerSigningKeys = null;

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, signingKey.X5t, tvp, null);
Assert.Same(resolvedKey, tvp.IssuerSigningKey);

// signingKey.X5t matches tvp.IssuerSigningKey since X509SecurityKey comparison is case-insensitive
resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, signingKey.X5t.ToUpper(), tvp, null);
Assert.Same(resolvedKey, tvp.IssuerSigningKey);

// signingKey.X5t matches TVP.IssuerSigningKeys.First()
signingKey.KeyId = Guid.NewGuid().ToString();
tvp.IssuerSigningKey = null;
tvp.IssuerSigningKeys = new List<SecurityKey>() { signingKey };

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, signingKey.X5t, tvp, null);
Assert.Same(resolvedKey, tvp.IssuerSigningKeys.First());

// signingKey.X5t matches configuration.SigningKeys.First()
signingKey.KeyId = Guid.NewGuid().ToString();
tvp.IssuerSigningKey = null;
tvp.IssuerSigningKeys = null;
configuration = GetConfigurationMock();

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(testKeyId, signingKey.X5t, tvp, configuration);
Assert.Same(resolvedKey, configuration.SigningKeys.First());

#endregion

// no signing key resolved
resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(Guid.NewGuid().ToString(), Guid.NewGuid().ToString(), tvp, null);
Assert.Null(resolvedKey);

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(null, null, tvp, null);
Assert.Null(resolvedKey);

resolvedKey = JwtTokenUtilities.ResolveTokenSigningKey(null, null, tvp, GetConfigurationNoMatchingKeyMock());
Assert.Null(resolvedKey);
}

private BaseConfiguration GetConfigurationMock()
{
var config = new OpenIdConnectConfiguration();
config.SigningKeys.Add(KeyingMaterial.X509SecurityKeySelfSigned1024_SHA256_Public);
config.SigningKeys.Add(KeyingMaterial.X509SecurityKeySelfSigned2048_SHA384_Public);

return config;
}

private BaseConfiguration GetConfigurationNoMatchingKeyMock()
{
var config = new OpenIdConnectConfiguration();
config.SigningKeys.Add(KeyingMaterial.DefaultRsaSecurityKey1);
config.SigningKeys.Add(KeyingMaterial.DefaultRsaSecurityKey2);

return config;
}
}
}

0 comments on commit 2a1cf33

Please sign in to comment.