diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 122c7ebec4..7c65073e9e 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -84,13 +84,23 @@ internal async ValueTask ValidateJWSAsync( if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) { - var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters, configuration); tokenValidationResult = await ValidateTokenPayloadAsync( validatedToken, validationParameters, configuration).ConfigureAwait(false); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters, configuration); + } + else if (validationParameters.SignatureValidatorWithToken != null + && ValidateSignatureUsingTokenDelegate(jsonWebToken, validationParameters, configuration) is { } delegateValidatedToken) + { + tokenValidationResult = await ValidateTokenPayloadAsync( + delegateValidatedToken, + validationParameters, + configuration).ConfigureAwait(false); + + Validators.ValidateIssuerSecurityKey(delegateValidatedToken.SigningKey, delegateValidatedToken, validationParameters, configuration); } else { @@ -436,12 +446,10 @@ internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey ke } } - private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) + private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) { if (validationParameters.SignatureValidatorUsingConfiguration != null) { - // TODO - get configuration from validationParameters - BaseConfiguration configuration = null; var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); if (validatedToken == null) throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); @@ -466,6 +474,43 @@ private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWeb throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); } + /// + /// Invokes the delegate + /// and returns the validated token if the delegate handled the signature, or + /// if the delegate declined. + /// + private JsonWebToken ValidateSignatureUsingTokenDelegate(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + try + { + var delegateResult = validationParameters.SignatureValidatorWithToken(jsonWebToken, validationParameters, configuration); + + if (!delegateResult.Handled) + return null; + + if (delegateResult.Token is not JsonWebToken validatedJsonWebToken) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(delegateResult.Token?.GetType()), jsonWebToken))); + + RecordSignatureValidationTelemetry( + TelemetryClient, + TelemetryConstants.SignatureValidationErrors.None, + jsonWebToken, + validatedJsonWebToken.SigningKey); + + return validatedJsonWebToken; + } + catch + { + RecordSignatureValidationTelemetry( + TelemetryClient, + TelemetryConstants.SignatureValidationErrors.SignatureVerificationFailed, + jsonWebToken, + jsonWebToken.SigningKey); + + throw; + } + } + /// /// Validates a JWS or a JWE. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Delegates.cs b/src/Microsoft.IdentityModel.Tokens/Delegates.cs index 42cc1d210b..73ae3346e6 100644 --- a/src/Microsoft.IdentityModel.Tokens/Delegates.cs +++ b/src/Microsoft.IdentityModel.Tokens/Delegates.cs @@ -136,6 +136,28 @@ namespace Microsoft.IdentityModel.Tokens /// The validated . public delegate SecurityToken SignatureValidatorUsingConfiguration(string token, TokenValidationParameters validationParameters, BaseConfiguration configuration); + /// + /// Validates the signature of an already-parsed token, with the ability to decline handling. + /// + /// The parsed . + /// The to be used for validating the token. + /// The configuration required for validation. + /// + /// A indicating whether the delegate handled the + /// signature validation. Return with the + /// validated token (and set) when the delegate validates + /// the signature. Return to let the + /// handler fall through to its default signature validation logic. Throw an appropriate exception + /// (e.g., ) if the signature is invalid. + /// + /// + /// This delegate is evaluated only when + /// and are not set. + /// Unlike those delegates, this one receives the already-parsed and + /// can decline to handle the signature, allowing the handler to validate it using its built-in logic. + /// + public delegate SignatureValidationDelegateResult SignatureValidatorWithToken(SecurityToken token, TokenValidationParameters validationParameters, BaseConfiguration configuration); + /// /// Reads the security token. /// diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net10.0/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net462/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net472/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net6.0/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net8.0/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/net9.0/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt index adef10032b..b5ca4c6512 100644 --- a/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/PublicAPI/netstandard2.0/PublicAPI.Unshipped.txt @@ -1 +1,16 @@ ~Microsoft.IdentityModel.Tokens.CaseSensitiveClaimsIdentity.SecurityToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Handled.get -> bool +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.SignatureValidationDelegateResult() -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Token.get -> Microsoft.IdentityModel.Tokens.SecurityToken? +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.NotHandled.get -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Success(Microsoft.IdentityModel.Tokens.SecurityToken token) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +~virtual Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken.Invoke(Microsoft.IdentityModel.Tokens.SecurityToken token, Microsoft.IdentityModel.Tokens.TokenValidationParameters validationParameters, Microsoft.IdentityModel.Tokens.BaseConfiguration configuration) -> Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.get -> Microsoft.IdentityModel.Tokens.SignatureValidatorWithToken +Microsoft.IdentityModel.Tokens.TokenValidationParameters.SignatureValidatorWithToken.set -> void +Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult other) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.Equals(object obj) -> bool +override Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.GetHashCode() -> int +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator ==(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool +static Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult.operator !=(Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult left, Microsoft.IdentityModel.Tokens.SignatureValidationDelegateResult right) -> bool diff --git a/src/Microsoft.IdentityModel.Tokens/SignatureValidationDelegateResult.cs b/src/Microsoft.IdentityModel.Tokens/SignatureValidationDelegateResult.cs new file mode 100644 index 0000000000..2e178acba1 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/SignatureValidationDelegateResult.cs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +#nullable enable + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Represents the result of a delegate invocation. + /// + /// + /// + /// This type allows a signature validation delegate to indicate whether it handled the signature + /// validation or whether it declined, allowing the handler to fall through to its default + /// signature validation logic. + /// + /// + /// Use when the delegate has validated the signature successfully. + /// Use when the delegate does not handle the token's algorithm and + /// wants the handler to validate the signature using its built-in logic. + /// If the delegate determines the signature is invalid, it should throw an appropriate exception + /// (e.g., ). + /// + /// + public readonly struct SignatureValidationDelegateResult : IEquatable + { + /// + /// Gets a value indicating whether the delegate handled the signature validation. + /// + /// + /// if the delegate validated the signature; + /// if the delegate declined and the handler should validate the signature using its default logic. + /// + public bool Handled { get; } + + /// + /// Gets the validated when is . + /// + /// + /// The validated token with set by the delegate, + /// or when is . + /// + public SecurityToken? Token { get; } + + private SignatureValidationDelegateResult(SecurityToken token) + { + Handled = true; + Token = token; + } + + /// + /// Returns a result indicating the delegate did not handle the signature validation. + /// The handler will fall through to its default signature validation logic. + /// + public static SignatureValidationDelegateResult NotHandled => default; + + /// + /// Returns a result indicating the delegate successfully validated the signature. + /// + /// The validated with + /// set to the key used for validation. + /// A indicating successful validation. + /// Thrown if is . + public static SignatureValidationDelegateResult Success(SecurityToken token) + => new(token ?? throw new ArgumentNullException(nameof(token))); + + /// + public override bool Equals(object? obj) => + obj is SignatureValidationDelegateResult other && Equals(other); + + /// + public bool Equals(SignatureValidationDelegateResult other) => + Handled == other.Handled && ReferenceEquals(Token, other.Token); + + /// + public override int GetHashCode() => + Handled.GetHashCode() ^ (Token?.GetHashCode() ?? 0); + + /// Equality operator. + public static bool operator ==(SignatureValidationDelegateResult left, SignatureValidationDelegateResult right) => + left.Equals(right); + + /// Inequality operator. + public static bool operator !=(SignatureValidationDelegateResult left, SignatureValidationDelegateResult right) => + !left.Equals(right); + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index fa6b589762..66e18a9cb4 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -84,6 +84,7 @@ protected TokenValidationParameters(TokenValidationParameters other) SaveSigninToken = other.SaveSigninToken; SignatureValidator = other.SignatureValidator; SignatureValidatorUsingConfiguration = other.SignatureValidatorUsingConfiguration; + SignatureValidatorWithToken = other.SignatureValidatorWithToken; TokenDecryptionKey = other.TokenDecryptionKey; TokenDecryptionKeyResolver = other.TokenDecryptionKeyResolver; TokenDecryptionKeys = other.TokenDecryptionKeys; @@ -556,6 +557,25 @@ public string RoleClaimType /// public SignatureValidatorUsingConfiguration SignatureValidatorUsingConfiguration { get; set; } + /// + /// Gets or sets a delegate that will be used to validate the signature of an already-parsed token. + /// + /// + /// + /// This delegate is evaluated only when and + /// are not set. Unlike those delegates, + /// this one receives the already-parsed and can decline to handle + /// the signature by returning , + /// allowing the handler to validate the signature using its built-in logic. + /// + /// + /// When the delegate handles the signature, it should set + /// on the token and return . + /// If the signature is invalid, the delegate should throw an appropriate exception. + /// + /// + public SignatureValidatorWithToken SignatureValidatorWithToken { get; set; } + /// /// Gets or sets the time provider. /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.SignatureValidatorWithTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.SignatureValidatorWithTokenTests.cs new file mode 100644 index 0000000000..bbf17e3a84 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.SignatureValidatorWithTokenTests.cs @@ -0,0 +1,427 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Protocols; +using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +namespace Microsoft.IdentityModel.JsonWebTokens.Tests +{ + public class JsonWebTokenHandlerSignatureValidatorWithTokenTests + { + private static JsonWebTokenHandler CreateHandler() => new JsonWebTokenHandler(); + + private static string CreateSignedToken(SecurityKey signingKey, string algorithm) + { + var handler = CreateHandler(); + return handler.CreateToken(new SecurityTokenDescriptor + { + SigningCredentials = new SigningCredentials(signingKey, algorithm), + Issuer = "https://test-issuer.example.com", + Audience = "https://test-audience.example.com" + }); + } + + private static TokenValidationParameters CreateBaseValidationParameters(SecurityKey signingKey) + { + return new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + IssuerSigningKey = signingKey, + ValidateLifetime = false + }; + } + + [Fact] + public async Task DelegateHandlesSignature_ValidationSucceeds() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return SignatureValidationDelegateResult.Success(jwt); + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + } + + [Fact] + public async Task DelegateDeclines_HandlerValidatesNormally() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + bool delegateWasCalled = false; + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + delegateWasCalled = true; + return SignatureValidationDelegateResult.NotHandled; + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(delegateWasCalled, "Delegate should have been called."); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + } + + [Fact] + public async Task DelegateThrows_ExceptionPropagatesAsInvalidResult() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + throw new SecurityTokenInvalidSignatureException("Signature is invalid."); + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.False(result.IsValid); + Assert.IsType(result.Exception); + } + + [Fact] + public async Task DelegateDeclines_WrongKey_ValidationFails() + { + // Arrange — sign with one key, validate with another + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var wrongKey = KeyingMaterial.RsaSecurityKey_4096; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var tvp = CreateBaseValidationParameters(wrongKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + // Decline — let the handler try (and fail) with the wrong key + return SignatureValidationDelegateResult.NotHandled; + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.False(result.IsValid); + } + + [Fact] + public async Task OldDelegatesTakePriority_NewDelegateNotCalled() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + bool newDelegateCalled = false; + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorUsingConfiguration = (token, validationParameters, configuration) => + { + var jwt = new JsonWebToken(token); + jwt.SigningKey = validationParameters.IssuerSigningKey; + return jwt; + }; + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + newDelegateCalled = true; + return SignatureValidationDelegateResult.Success((JsonWebToken)token); + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.False(newDelegateCalled, "New delegate should not be called when old delegate is set."); + } + + [Fact] + public async Task DelegateReceivesResolvedBaseConfiguration() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + BaseConfiguration capturedConfiguration = null; + + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.ConfigurationManager = new StaticConfigurationManager(expectedConfiguration); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + capturedConfiguration = configuration; + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return SignatureValidationDelegateResult.Success(jwt); + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.NotNull(capturedConfiguration); + Assert.Same(expectedConfiguration, capturedConfiguration); + } + + [Fact] + public async Task DelegateDeclines_ConfigurationPassedToHandlerSignatureValidation() + { + // Arrange — use ConfigurationManager as the only source of signing keys + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + var tvp = new TokenValidationParameters + { + ValidIssuer = "https://test-issuer.example.com", + ValidAudience = "https://test-audience.example.com", + ValidateLifetime = false, + ConfigurationManager = new StaticConfigurationManager(expectedConfiguration), + SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + // Decline — the handler should use configuration to resolve signing key + return SignatureValidationDelegateResult.NotHandled; + } + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + } + + [Fact] + public async Task DelegateHandled_IssuerSigningKeyValidation_ReceivesConfiguration() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + BaseConfiguration capturedKeyValidationConfig = null; + + var expectedConfiguration = new OpenIdConnectConfiguration + { + Issuer = "https://test-issuer.example.com" + }; + expectedConfiguration.SigningKeys.Add(signingKey); + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.ValidateIssuerSigningKey = true; + tvp.ConfigurationManager = new StaticConfigurationManager(expectedConfiguration); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return SignatureValidationDelegateResult.Success(jwt); + }; + tvp.IssuerSigningKeyValidatorUsingConfiguration = (securityKey, securityToken, tvpInner, configuration) => + { + capturedKeyValidationConfig = configuration; + return true; + }; + + // Act + var result = await CreateHandler().ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.NotNull(capturedKeyValidationConfig); + Assert.Same(expectedConfiguration, capturedKeyValidationConfig); + } + + [Fact] + public async Task DelegateHandled_TelemetryRecordsSuccess() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var mockTelemetry = new SignatureValidationMockTelemetryClient(); + var handler = new JsonWebTokenHandler + { + TelemetryClient = mockTelemetry + }; + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return SignatureValidationDelegateResult.Success(jwt); + }; + + // Act + Telemetry.CryptoTelemetry.EnableSignatureValidationTelemetry(true, null); + try + { + var result = await handler.ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + Assert.Equal(1, mockTelemetry.SignatureValidationCallCount); + Assert.Equal(Telemetry.TelemetryConstants.SignatureValidationErrors.None, mockTelemetry.LastErrorType); + } + finally + { + Telemetry.CryptoTelemetry.EnableSignatureValidationTelemetry(false, null); + } + } + + [Fact] + public async Task DelegateThrows_TelemetryRecordsFailure() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + var mockTelemetry = new SignatureValidationMockTelemetryClient(); + var handler = new JsonWebTokenHandler + { + TelemetryClient = mockTelemetry + }; + + var tvp = CreateBaseValidationParameters(signingKey); + tvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + throw new SecurityTokenInvalidSignatureException("Bad signature."); + }; + + // Act + Telemetry.CryptoTelemetry.EnableSignatureValidationTelemetry(true, null); + try + { + var result = await handler.ValidateTokenAsync(tokenString, tvp); + + // Assert + Assert.False(result.IsValid); + Assert.Equal(1, mockTelemetry.SignatureValidationCallCount); + Assert.Equal(Telemetry.TelemetryConstants.SignatureValidationErrors.SignatureVerificationFailed, mockTelemetry.LastErrorType); + } + finally + { + Telemetry.CryptoTelemetry.EnableSignatureValidationTelemetry(false, null); + } + } + + [Fact] + public void SignatureValidationDelegateResult_NotHandled_HasCorrectDefaults() + { + // Act + var result = SignatureValidationDelegateResult.NotHandled; + + // Assert + Assert.False(result.Handled); + Assert.Null(result.Token); + } + + [Fact] + public void SignatureValidationDelegateResult_Success_SetsProperties() + { + // Arrange + var handler = CreateHandler(); + var tokenString = CreateSignedToken(KeyingMaterial.RsaSecurityKey_2048, SecurityAlgorithms.RsaSha256); + var token = new JsonWebToken(tokenString); + + // Act + var result = SignatureValidationDelegateResult.Success(token); + + // Assert + Assert.True(result.Handled); + Assert.Same(token, result.Token); + } + + [Fact] + public void SignatureValidationDelegateResult_Success_NullToken_ThrowsArgumentNullException() + { + // Act & Assert + Assert.Throws(() => SignatureValidationDelegateResult.Success(null)); + } + + [Fact] + public async Task CopyConstructor_CopiesSignatureValidatorWithToken() + { + // Arrange + var signingKey = KeyingMaterial.RsaSecurityKey_2048; + var tokenString = CreateSignedToken(signingKey, SecurityAlgorithms.RsaSha256); + + bool delegateCalled = false; + var originalTvp = CreateBaseValidationParameters(signingKey); + originalTvp.SignatureValidatorWithToken = (token, validationParameters, configuration) => + { + delegateCalled = true; + var jwt = (JsonWebToken)token; + jwt.SigningKey = validationParameters.IssuerSigningKey; + return SignatureValidationDelegateResult.Success(jwt); + }; + + // Act — clone via copy constructor + var clonedTvp = originalTvp.Clone(); + var result = await CreateHandler().ValidateTokenAsync(tokenString, clonedTvp); + + // Assert + Assert.True(delegateCalled, "Delegate from cloned TVP should have been called."); + Assert.True(result.IsValid, $"Validation failed: {result.Exception?.Message}"); + } + + /// + /// A minimal mock that captures signature validation telemetry calls. + /// Inherits from and overrides nothing + /// except signature validation counter to capture the call. + /// + private class SignatureValidationMockTelemetryClient : Telemetry.ITelemetryClient + { + public int SignatureValidationCallCount { get; private set; } + public string LastErrorType { get; private set; } + public string LastAlgorithm { get; private set; } + + void Telemetry.ITelemetryClient.IncrementSignatureValidationCounter(string errorType, string issuer, string algorithm, SecurityKey key) + { + SignatureValidationCallCount++; + LastErrorType = errorType; + LastAlgorithm = algorithm; + } + + void Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, string configurationSource) { } + void Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, string configurationSource, Exception exception) { } + void Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus, Exception exception) { } + void Telemetry.ITelemetryClient.IncrementConfigurationRefreshRequestCounter(string metadataAddress, string operationStatus) { } + void Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, string configurationSource, TimeSpan operationDuration) { } + void Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, string configurationSource, TimeSpan operationDuration, Exception exception) { } + void Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration) { } + void Telemetry.ITelemetryClient.LogConfigurationRetrievalDuration(string metadataAddress, TimeSpan operationDuration, Exception exception) { } + void Telemetry.ITelemetryClient.LogBackgroundConfigurationRefreshFailure(string metadataAddress, string configurationSource, Exception exception) { } + void Telemetry.ITelemetryClient.LogBackgroundConfigurationRefreshFailure(string metadataAddress, Exception exception) { } + } + } +}