Skip to content

Commit

Permalink
Introduces AadSigningKeyIssuerValidation check (#2136)
Browse files Browse the repository at this point in the history
  • Loading branch information
sruke authored Jul 12, 2023
1 parent 2030afc commit 8f00ee2
Show file tree
Hide file tree
Showing 7 changed files with 505 additions and 4 deletions.
10 changes: 10 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/Validators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,16 @@ internal static void ValidateIssuerSecurityKey(SecurityKey securityKey, Security
if (securityToken == null)
throw LogHelper.LogArgumentNullException(nameof(securityToken));

ValidateIssuerSigningKeyLifeTime(securityKey, validationParameters);
}

/// <summary>
/// Given a signing key, when it's derived from a certificate, validates that the certificate is already active and non-expired
/// </summary>
/// <param name="securityKey">The <see cref="SecurityKey"/> that signed the <see cref="SecurityToken"/>.</param>
/// <param name="validationParameters">The <see cref="TokenValidationParameters"/> that are used to validate the token.</param>
internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, TokenValidationParameters validationParameters)
{
X509SecurityKey x509SecurityKey = securityKey as X509SecurityKey;
if (x509SecurityKey?.Certificate is X509Certificate2 cert)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class AadIssuerValidator
private static readonly TimeSpan LastKnownGoodConfigurationLifetime = new TimeSpan(0, 24, 0, 0);

internal const string V2EndpointSuffix = "/v2.0";
internal const string TenantidTemplate = "{tenantid}";
internal const string TenantIdTemplate = "{tenantid}";

internal AadIssuerValidator(
HttpClient httpClient,
Expand Down Expand Up @@ -292,9 +292,9 @@ private static bool IsValidIssuer(string validIssuerTemplate, string tenantId, s
if (string.IsNullOrEmpty(validIssuerTemplate))
return false;

if (validIssuerTemplate.Contains(TenantidTemplate))
if (validIssuerTemplate.Contains(TenantIdTemplate))
{
return validIssuerTemplate.Replace(TenantidTemplate, tenantId) == actualIssuer;
return validIssuerTemplate.Replace(TenantIdTemplate, tenantId) == actualIssuer;
}
else
{
Expand All @@ -311,7 +311,7 @@ private BaseConfigurationManager GetEffectiveConfigurationManager(SecurityToken
/// <param name="securityToken">A JWT token.</param>
/// <returns>A string containing the tenant ID, if found or <see cref="string.Empty"/>.</returns>
/// <remarks>Only <see cref="JwtSecurityToken"/> and <see cref="JsonWebToken"/> are acceptable types.</remarks>
private static string GetTenantIdFromToken(SecurityToken securityToken)
internal static string GetTenantIdFromToken(SecurityToken securityToken)
{
if (securityToken is JwtSecurityToken jwtSecurityToken)
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Linq;
using Microsoft.IdentityModel.Logging;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using Microsoft.IdentityModel.Tokens;

namespace Microsoft.IdentityModel.Validators
{
/// <summary>
/// A generic class for additional validation checks on <see cref="SecurityToken"/> issued by the Microsoft identity platform (AAD).
/// </summary>
public static class AadTokenValidationParametersExtension
{
/// <summary>
/// Enables the validation of the issuer of the signing keys used by the Microsoft identity platform (AAD) against the issuer of the token.
/// </summary>
/// <param name="tokenValidationParameters">The <see cref="TokenValidationParameters"/> that are used to validate the token.</param>
public static void EnableAadSigningKeyIssuerValidation(this TokenValidationParameters tokenValidationParameters)
{
if (tokenValidationParameters == null)
throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters));

var userProvidedIssuerSigningKeyValidatorUsingConfiguration = tokenValidationParameters.IssuerSigningKeyValidatorUsingConfiguration;
var userProvidedIssuerSigningKeyValidator = tokenValidationParameters.IssuerSigningKeyValidator;

tokenValidationParameters.IssuerSigningKeyValidatorUsingConfiguration = (securityKey, securityToken, tvp, config) =>
{
ValidateIssuerSigningKey(securityKey, securityToken, config);

// preserve and run provided logic
if (userProvidedIssuerSigningKeyValidatorUsingConfiguration != null)
return userProvidedIssuerSigningKeyValidatorUsingConfiguration(securityKey, securityToken, tvp, config);

if (userProvidedIssuerSigningKeyValidator != null)
return userProvidedIssuerSigningKeyValidator(securityKey, securityToken, tvp);

return ValidateIssuerSigningKeyCertificate(securityKey, tvp);
};
}

/// <summary>
/// Validates the issuer signing key.
/// </summary>
/// <param name="securityKey">The <see cref="SecurityKey"/> that signed the <see cref="SecurityToken"/>.</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> being validated, could be a JwtSecurityToken or JsonWebToken.</param>
/// <param name="configuration">The <see cref="OpenIdConnectConfiguration"/> provided.</param>
/// <returns><c>true</c> if the issuer signing key is valid; otherwise, <c>false</c>.</returns>
internal static bool ValidateIssuerSigningKey(SecurityKey securityKey, SecurityToken securityToken, BaseConfiguration configuration)
{
if (securityKey == null)
return true;

if (securityToken == null)
throw LogHelper.LogArgumentNullException(nameof(securityToken));

var openIdConnectConfiguration = configuration as OpenIdConnectConfiguration;
if (openIdConnectConfiguration == null)
return true;

var matchedKeyFromConfig = openIdConnectConfiguration.JsonWebKeySet?.Keys.FirstOrDefault(x => x != null && x.Kid == securityKey.KeyId);
if (matchedKeyFromConfig != null && matchedKeyFromConfig.AdditionalData.TryGetValue(OpenIdProviderMetadataNames.Issuer, out object value))
{
var signingKeyIssuer = value as string;
if (string.IsNullOrWhiteSpace(signingKeyIssuer))
return true;

var tenantIdFromToken = AadIssuerValidator.GetTenantIdFromToken(securityToken);
if (string.IsNullOrEmpty(tenantIdFromToken))
return true;

var tokenIssuer = securityToken.Issuer;

#if NET6_0_OR_GREATER
if (!string.IsNullOrEmpty(tokenIssuer) && !tokenIssuer.Contains(tenantIdFromToken, StringComparison.Ordinal))
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40004, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(tenantIdFromToken))));

// creating an effectiveSigningKeyIssuer is required as signingKeyIssuer might contain {tenantid}
var effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, StringComparison.Ordinal);
var v2TokenIssuer = openIdConnectConfiguration.Issuer?.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, StringComparison.Ordinal);
#else
if (!string.IsNullOrEmpty(tokenIssuer) && !tokenIssuer.Contains(tenantIdFromToken))
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40004, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(tenantIdFromToken))));

// creating an effectiveSigningKeyIssuer is required as signingKeyIssuer might contain {tenantid}
var effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken);
var v2TokenIssuer = openIdConnectConfiguration.Issuer?.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken);
#endif

// comparing effectiveSigningKeyIssuer with v2TokenIssuer is required as well because of the following scenario:
// 1. service trusts /common/v2.0 endpoint
// 2. service receieves a v1 token that has issuer like sts.windows.net
// 3. signing key issuers will never match sts.windows.net as v1 endpoint doesn't have issuers attached to keys
// v2TokenIssuer is the representation of Token.Issuer (if it was a v2 issuer)
if (effectiveSigningKeyIssuer != tokenIssuer && effectiveSigningKeyIssuer != v2TokenIssuer)
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogHelper.FormatInvariant(LogMessages.IDX40005, LogHelper.MarkAsNonPII(tokenIssuer), LogHelper.MarkAsNonPII(effectiveSigningKeyIssuer))));
}

return true;
}

/// <summary>
/// Validates the issuer signing key certificate.
/// </summary>
/// <param name="securityKey">The <see cref="SecurityKey"/> that signed the <see cref="SecurityToken"/>.</param>
/// <param name="validationParameters">The <see cref="TokenValidationParameters"/> that are used to validate the token.</param>
/// <returns><c>true</c> if the issuer signing key certificate is valid; otherwise, <c>false</c>.</returns>
internal static bool ValidateIssuerSigningKeyCertificate(SecurityKey securityKey, TokenValidationParameters validationParameters)
{
if (!validationParameters.RequireSignedTokens && securityKey == null)
{
LogHelper.LogInformation(Tokens.LogMessages.IDX10252);
return true;
}
else if (securityKey == null)
{
throw LogHelper.LogExceptionMessage(new ArgumentNullException(nameof(securityKey), LogMessages.IDX40007));
}

if (!validationParameters.ValidateIssuerSigningKey)
{
LogHelper.LogVerbose(Tokens.LogMessages.IDX10237);
return true;
}

Tokens.Validators.ValidateIssuerSigningKeyLifeTime(securityKey, validationParameters);

return true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,8 @@ internal static class LogMessages

// Protocol
public const string IDX40003 = "IDX40003: Neither `tid` nor `tenantId` claim is present in the token obtained from Microsoft identity platform. ";
public const string IDX40004 = "IDX40004: Token issuer: '{0}', does not contain the `tid` or `tenantId` claim present in the token: '{1}'.";
public const string IDX40005 = "IDX40005: Token issuer: '{0}', does not match the signing key issuer: '{1}'.";
public const string IDX40007 = "IDX40007: RequireSignedTokens property on ValidationParameters is set to true, but the issuer signing key is null.";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.IdentityModel.Protocols.OpenIdConnect\Microsoft.IdentityModel.Protocols.OpenIdConnect.csproj" />
<ProjectReference Include="..\Microsoft.IdentityModel.Protocols\Microsoft.IdentityModel.Protocols.csproj" />
<ProjectReference Include="..\Microsoft.IdentityModel.Tokens\Microsoft.IdentityModel.Tokens.csproj" />
<ProjectReference Include="..\System.IdentityModel.Tokens.Jwt\System.IdentityModel.Tokens.Jwt.csproj" />
</ItemGroup>

Expand Down
Loading

0 comments on commit 8f00ee2

Please sign in to comment.