Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduces AadSigningKeyIssuerValidation check #2136

Merged
merged 1 commit into from
Jul 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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