Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static class OpenIdProviderMetadataNames
public const string IntrospectionEndpointAuthSigningAlgValuesSupported = "introspection_endpoint_auth_signing_alg_values_supported";
public const string JwksUri = "jwks_uri";
public const string Issuer = "issuer";
public const string CloudInstanceName = "cloud_instance_name";
public const string LogoutSessionSupported = "logout_session_supported";
public const string MicrosoftMultiRefreshToken = "microsoft_multi_refresh_token";
public const string OpPolicyUri = "op_policy_uri";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Runtime.Serialization;

namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// This exception is thrown when specific cloud instance was not matched with cloud instance of signing key.
/// </summary>
[Serializable]
public class SecurityTokenInvalidCloudInstanceException : SecurityTokenInvalidSigningKeyException
{
[NonSerialized]
const string _Prefix = "Microsoft.IdentityModel." + nameof(SecurityTokenInvalidCloudInstanceException) + ".";

[NonSerialized]
const string _InvalidCloudInstanceKey = _Prefix + nameof(InvalidCloudInstance);

/// <summary>
/// Gets or sets the invalid cloud instance that created the validation exception.
/// </summary>
public string InvalidCloudInstance { get; set; }

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidCloudInstanceException"/> class.
/// </summary>
public SecurityTokenInvalidCloudInstanceException()
: base()
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidCloudInstanceException"/> class.
/// </summary>
/// <param name="message">Addtional information to be included in the exception and displayed to user.</param>
public SecurityTokenInvalidCloudInstanceException(string message)
: base(message)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidCloudInstanceException"/> class.
/// </summary>
/// <param name="message">Addtional information to be included in the exception and displayed to user.</param>
/// <param name="innerException">A <see cref="Exception"/> that represents the root cause of the exception.</param>
public SecurityTokenInvalidCloudInstanceException(string message, Exception innerException)
: base(message, innerException)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="SecurityTokenInvalidCloudInstanceException"/> class.
/// </summary>
/// <param name="info">the <see cref="SerializationInfo"/> that holds the serialized object data.</param>
/// <param name="context">The contextual information about the source or destination.</param>
#if NET8_0_OR_GREATER
[Obsolete("Formatter-based serialization is obsolete", DiagnosticId = "SYSLIB0051")]
#endif
protected SecurityTokenInvalidCloudInstanceException(SerializationInfo info, StreamingContext context)
: base(info, context)
{
SerializationInfoEnumerator enumerator = info.GetEnumerator();
while (enumerator.MoveNext())
{
switch (enumerator.Name)
{
case _InvalidCloudInstanceKey:
InvalidCloudInstance = info.GetString(_InvalidCloudInstanceKey);
break;

default:
// Ignore other fields.
break;
}
}
}

/// <inheritdoc/>
#if NET8_0_OR_GREATER
[Obsolete("Formatter-based serialization is obsolete", DiagnosticId = "SYSLIB0051")]
#endif
public override void GetObjectData(SerializationInfo info, StreamingContext context)
{
base.GetObjectData(info, context);

if (!string.IsNullOrEmpty(InvalidCloudInstance))
info.AddValue(_InvalidCloudInstanceKey, InvalidCloudInstance);
}
}
}
14 changes: 13 additions & 1 deletion src/Microsoft.IdentityModel.Tokens/SecurityKey.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// Licensed under the MIT License.

using System;
using Microsoft.IdentityModel.Logging;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using System.Threading;
using Microsoft.IdentityModel.Logging;

namespace Microsoft.IdentityModel.Tokens
{
Expand All @@ -15,6 +17,7 @@ public abstract class SecurityKey
private CryptoProviderFactory _cryptoProviderFactory;
private object _internalIdLock = new object();
private string _internalId;
private IDictionary<string, object> _propertyBag;

internal SecurityKey(SecurityKey key)
{
Expand Down Expand Up @@ -80,6 +83,15 @@ public CryptoProviderFactory CryptoProviderFactory
}
}

/// <summary>
/// Gets or sets the <see cref="IDictionary{String, Object}"/> that contains a collection of custom key/value pairs. This allows addition of data that could be used in custom scenarios. This uses <see cref="StringComparer.Ordinal"/> for case-sensitive comparison of keys.
/// </summary>
[JsonIgnore]
public IDictionary<string, object> PropertyBag =>
_propertyBag ??
Interlocked.CompareExchange(ref _propertyBag, new Dictionary<string, object>(StringComparer.Ordinal), null) ??
_propertyBag;

/// <summary>
/// Returns the formatted string: GetType(), KeyId: 'value', InternalId: 'value'.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,51 @@ namespace Microsoft.IdentityModel.Validators
/// </summary>
public static class AadTokenValidationParametersExtension
{
/// <summary>
/// Enables validation of Microsoft Entra ID token signing keys.
/// </summary>
/// <param name="tokenValidationParameters">The <see cref="TokenValidationParameters"/> that are used to validate the token.</param>
/// <param name="cloudInstanceName">The optional cloud instance name to validate against.</param>
public static void EnableEntraIdSigningKeyValidation(this TokenValidationParameters tokenValidationParameters, string cloudInstanceName = null)
{
if (tokenValidationParameters == null)
throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters));

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

tokenValidationParameters.IssuerSigningKeyValidatorUsingConfiguration = (securityKey, securityToken, tvp, config) =>
{
ValidateSigningKeyCloudInstanceName(securityKey, config, cloudInstanceName);
ValidateSigningKeyIssuer(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>
/// 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>
[Obsolete("Use EnableAadSigningKeyValidation(TokenValidationParameters, string) instead.")]
public static void EnableAadSigningKeyIssuerValidation(this TokenValidationParameters tokenValidationParameters)
{
if (tokenValidationParameters == null)
throw LogHelper.LogArgumentNullException(nameof(tokenValidationParameters));

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

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

// preserve and run provided logic
if (userProvidedIssuerSigningKeyValidatorUsingConfiguration != null)
Expand All @@ -49,28 +79,27 @@ public static void EnableAadSigningKeyIssuerValidation(this TokenValidationParam
/// </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)
/// <param name="configuration">The <see cref="BaseConfiguration"/> provided.</param>
/// <returns><c>true</c> if the issuer of the signing key is valid; otherwise, <c>false</c>.</returns>
internal static bool ValidateSigningKeyIssuer(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)
if (configuration is not OpenIdConnectConfiguration openIdConnectConfiguration)
return true;

var matchedKeyFromConfig = openIdConnectConfiguration.JsonWebKeySet?.Keys.FirstOrDefault(x => x != null && x.Kid == securityKey.KeyId);
JsonWebKey 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;
string signingKeyIssuer = value as string;
if (string.IsNullOrWhiteSpace(signingKeyIssuer))
return true;

var tenantIdFromToken = GetTid(securityToken);
string tenantIdFromToken = GetTid(securityToken);
if (string.IsNullOrEmpty(tenantIdFromToken))
{
if (AppContextSwitches.DontFailOnMissingTid)
Expand All @@ -79,22 +108,22 @@ internal static bool ValidateIssuerSigningKey(SecurityKey securityKey, SecurityT
throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX40009));
}

var tokenIssuer = securityToken.Issuer;
string 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);
string effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken, StringComparison.Ordinal);
string 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);
string effectiveSigningKeyIssuer = signingKeyIssuer.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken);
string v2TokenIssuer = openIdConnectConfiguration.Issuer?.Replace(AadIssuerValidator.TenantIdTemplate, tenantIdFromToken);
#endif

// comparing effectiveSigningKeyIssuer with v2TokenIssuer is required as well because of the following scenario:
Expand All @@ -109,6 +138,43 @@ internal static bool ValidateIssuerSigningKey(SecurityKey securityKey, SecurityT
return true;
}

/// <summary>
/// Validates the cloud instance name of the signing key in a property bag.
/// </summary>
/// <param name="securityKey">The <see cref="SecurityKey"/> that signed the <see cref="SecurityToken"/>.</param>
/// <param name="configuration">The <see cref="BaseConfiguration"/> provided.</param>
/// <param name="cloudInstanceName">The cloud instance name to validate against.</param>
internal static void ValidateSigningKeyCloudInstanceName(SecurityKey securityKey, BaseConfiguration configuration, string cloudInstanceName)
{
if (securityKey == null)
return;

if (configuration is not OpenIdConnectConfiguration openIdConnectConfiguration)
return;

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

// Store the cloud instance name in the security key's property bag.
securityKey.PropertyBag[OpenIdProviderMetadataNames.CloudInstanceName] = signingKeyCloudInstance;

if (cloudInstanceName == null)
return;

if (!string.Equals(signingKeyCloudInstance, cloudInstanceName, StringComparison.Ordinal))
throw LogHelper.LogExceptionMessage(
new SecurityTokenInvalidCloudInstanceException(LogHelper.FormatInvariant(LogMessages.IDX40012, LogHelper.MarkAsNonPII(cloudInstanceName), LogHelper.MarkAsNonPII(signingKeyCloudInstance)))
{
InvalidCloudInstance = cloudInstanceName,
SigningKey = securityKey,
});
}
}

private static string GetTid(SecurityToken securityToken)
{
switch (securityToken)
Expand Down
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Validators/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ internal static class LogMessages
public const string IDX40009 = "IDX40009: Either the 'tid' claim was not found or it didn't have a value.";
public const string IDX40010 = "IDX40010: The SecurityToken must be a 'JsonWebToken' or 'JwtSecurityToken'";
public const string IDX40011 = "IDX40011: The SecurityToken has multiple instances of the '{0}' claim.";
public const string IDX40012 = "IDX40012: The cloud instance name: '{0}', does not match cloud instance name of the signing key: '{1}'.";
}
}
5 changes: 5 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/Default.cs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,11 @@ public static string Issuer
get => "http://Default.Issuer.com";
}

public static string CloudInstanceName
{
get => "microsoftonline.com";
}

public static IEnumerable<string> Issuers
{
get => new List<string> {
Expand Down
5 changes: 5 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ public static ExpectedException SecurityTokenInvalidIssuerException(string subst
return new ExpectedException(typeof(SecurityTokenInvalidIssuerException), substringExpected, innerTypeExpected, propertiesExpected: propertiesExpected);
}

public static ExpectedException SecurityTokenInvalidCloudInstanceNameException(string substringExpected = null, Type innerTypeExpected = null, Dictionary<string, object> propertiesExpected = null)
{
return new ExpectedException(typeof(SecurityTokenInvalidCloudInstanceException), substringExpected, innerTypeExpected, propertiesExpected: propertiesExpected);
}

public static ExpectedException SecurityTokenKeyWrapException(string substringExpected = null, Type innerTypeExpected = null, Dictionary<string, object> propertiesExpected = null)
{
return new ExpectedException(typeof(SecurityTokenKeyWrapException), substringExpected, innerTypeExpected, propertiesExpected: propertiesExpected);
Expand Down
Loading
Loading