From e675885363b7bd3e6c5aad499231ccdcb085e732 Mon Sep 17 00:00:00 2001 From: id4s Date: Sun, 5 May 2024 13:32:51 -0700 Subject: [PATCH 1/2] Add framework for reducing exceptions and logging. --- Wilson.sln | 2 + buildPack.bat | 1 + buildTestPack.bat | 1 + .../JsonWebTokenHandler.ValidateToken.cs | 771 ++++++++++++++++++ .../JsonWebTokenHandler.cs | 545 ------------- .../Exceptions/SecurityTokenException.cs | 49 +- .../LogMessages.cs | 4 +- .../TokenValidationParameters.cs | 22 +- .../Validation/AsyncValidate.cd | 81 ++ .../Validation/ExceptionDetail.cs | 73 ++ .../Validation/Exceptions.cd | 35 + .../Validation/IssuerValidationResult.cs | 69 ++ .../Validation/LogDetail.cs | 34 + .../Validation/MessageDetail.cs | 46 ++ ...tionParameters.IssuerValidationDelegate.cs | 16 + .../{ => Validation}/TokenValidationResult.cs | 43 +- .../Validation/ValidationDelegates.cd | 21 + .../Validation/ValidationFailureType.cs | 49 ++ .../Validation/ValidationResult.cs | 110 +++ .../Validation/Validators.Audience.cs | 154 ++++ .../Validation/Validators.Issuer.cs | 293 +++++++ .../Validation/Validators.Lifetime.cs | 50 ++ .../ValidatorUtilities.cs | 4 +- .../Validators.cs | 317 +------ .../JsonWebTokenHandlerTests.cs | 2 +- .../IdentityComparer.cs | 116 ++- .../ValidationDelegates.cs | 2 +- .../IdentityComparerTests.cs | 6 +- .../Json/JsonUtilities.cs | 6 + .../TokenValidationParametersTests.cs | 6 +- .../Validation/AsyncValidatorsTests.cs | 58 ++ .../Validation/IssuerValidationResultTests.cs | 120 +++ .../{ => Validation}/ValidatorsTests.cs | 4 - 33 files changed, 2206 insertions(+), 904 deletions(-) create mode 100644 src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs rename src/Microsoft.IdentityModel.Tokens/{ => Validation}/TokenValidationResult.cs (85%) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs rename test/Microsoft.IdentityModel.Tokens.Tests/{ => Validation}/ValidatorsTests.cs (99%) diff --git a/Wilson.sln b/Wilson.sln index 9c671e6168..8dd5dcbce2 100644 --- a/Wilson.sln +++ b/Wilson.sln @@ -85,6 +85,8 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{4655DBB4-70C6-475D-8971-FE6619B85F70}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig + buildPack.bat = buildPack.bat + buildTestPack.bat = buildTestPack.bat EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.IdentityModel.Validators", "src\Microsoft.IdentityModel.Validators\Microsoft.IdentityModel.Validators.csproj", "{DA585910-0E6C-45A5-AABD-30917130FD63}" diff --git a/buildPack.bat b/buildPack.bat index 7e782b647f..6a4c89623c 100644 --- a/buildPack.bat +++ b/buildPack.bat @@ -1,2 +1,3 @@ +dotnet clean Product.proj > clean.log dotnet build /r Product.proj dotnet pack --no-restore -o artifacts --no-build Product.proj diff --git a/buildTestPack.bat b/buildTestPack.bat index 40e59fda52..b5dba1b86f 100644 --- a/buildTestPack.bat +++ b/buildTestPack.bat @@ -1,3 +1,4 @@ +dotnet clean Product.proj > clean.log dotnet build /r Product.proj dotnet test --no-restore --no-build Product.proj dotnet pack --no-restore -o artifacts --no-build Product.proj diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs new file mode 100644 index 0000000000..524559e198 --- /dev/null +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -0,0 +1,771 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Tokens; +using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages; + +namespace Microsoft.IdentityModel.JsonWebTokens +{ + /// + /// A designed for creating and validating Json Web Tokens. + /// See: https://datatracker.ietf.org/doc/html/rfc7519 and http://www.rfc-editor.org/info/rfc7515. + /// + public partial class JsonWebTokenHandler : TokenHandler + { + /// + /// Returns a value that indicates if this handler can validate a . + /// + /// 'true', indicating this instance can validate a . + public virtual bool CanValidateToken + { + get { return true; } + } + + internal async ValueTask ValidateJWEAsync( + JsonWebToken jwtToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters), validationParameters); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + + tokenValidationResult = await ValidateJWSAsync( + tokenValidationResult.SecurityToken as JsonWebToken, + validationParameters, + configuration).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; + return new TokenValidationResult + { + SecurityToken = jwtToken, + ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, + IsValid = true, + TokenType = tokenValidationResult.TokenType + }; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null + }; + } + } + + internal async ValueTask ValidateJWEAsync( + JsonWebToken jwtToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + try + { + TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters), validationParameters); + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + tokenValidationResult = await ValidateJWSAsync( + tokenValidationResult.SecurityToken as JsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + + jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; + jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; + return new TokenValidationResult + { + SecurityToken = jwtToken, + ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, + IsValid = true, + TokenType = tokenValidationResult.TokenType + }; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null + }; + } + } + + internal async ValueTask ValidateJWSAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + try + { + TokenValidationResult tokenValidationResult; + if (validationParameters.TransformBeforeSignatureValidation != null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + configuration).ConfigureAwait(false); + + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + } + else + { + if (validationParameters.ValidateSignatureLast) + { + tokenValidationResult = await ValidateTokenPayloadAsync( + jsonWebToken, + validationParameters, + configuration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + } + else + { + var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + configuration).ConfigureAwait(false); + } + } + + return tokenValidationResult; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null + }; + } + } + + internal async ValueTask ValidateJWSAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + try + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager != null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set + // directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + TokenValidationResult tokenValidationResult; + if (validationParameters.TransformBeforeSignatureValidation != null) + jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; + + if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) + { + var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters); + } + else + { + if (validationParameters.ValidateSignatureLast) + { + tokenValidationResult = await ValidateTokenPayloadAsync( + jsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, currentConfiguration); + } + else + { + var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, currentConfiguration); + tokenValidationResult = await ValidateTokenPayloadAsync( + validatedToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + } + } + + return tokenValidationResult; + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false, + TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null + }; + } + } + + private static JsonWebToken ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + JsonWebToken validatedToken = ValidateSignature(jsonWebToken, validationParameters, configuration); + Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, jsonWebToken, validationParameters, configuration); + return validatedToken; + } + + /// + /// Validates the JWT signature. + /// + private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + bool kidMatched = false; + IEnumerable keys = null; + + if (!jwtToken.IsSigned) + { + if (validationParameters.RequireSignedTokens) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, jwtToken))); + else + return jwtToken; + } + + if (validationParameters.IssuerSigningKeyResolverUsingConfiguration != null) + { + keys = validationParameters.IssuerSigningKeyResolverUsingConfiguration(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, configuration); + } + else if (validationParameters.IssuerSigningKeyResolver != null) + { + keys = validationParameters.IssuerSigningKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); + } + else + { + var key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters, configuration); + if (key != null) + { + kidMatched = true; + keys = [key]; + } + } + + if (validationParameters.TryAllIssuerSigningKeys && keys.IsNullOrEmpty()) + { + // control gets here if: + // 1. User specified delegate: IssuerSigningKeyResolver returned null + // 2. ResolveIssuerSigningKey returned null + // Try all the keys. This is the degenerate case, not concerned about perf. + keys = TokenUtilities.GetAllSigningKeys(configuration, validationParameters); + } + + // keep track of exceptions thrown, keys that were tried + StringBuilder exceptionStrings = null; + StringBuilder keysAttempted = null; + var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); + + if (keys != null) + { + foreach (var key in keys) + { +#pragma warning disable CA1031 // Do not catch general exception types + try + { + if (ValidateSignature(jwtToken, key, validationParameters)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); + + jwtToken.SigningKey = key; + return jwtToken; + } + } + catch (Exception ex) + { + (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); + } +#pragma warning restore CA1031 // Do not catch general exception types + + if (key != null) + { + (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); + if (kidExists && !kidMatched && key.KeyId != null) + kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); + } + } + } + + // Get information on where keys used during token validation came from for debugging purposes. + var keysInTokenValidationParameters = TokenUtilities.GetAllSigningKeys(validationParameters: validationParameters); + + var keysInConfiguration = TokenUtilities.GetAllSigningKeys(configuration); + var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count(); + var numKeysInConfiguration = keysInConfiguration.Count(); + + if (kidExists) + { + if (kidMatched) + { + JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path + var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); + var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + LogHelper.MarkAsNonPII(keyLocation), + LogHelper.MarkAsNonPII(jwtToken.Kid), + (object)exceptionStrings ?? "", + jwtToken))); + } + + if (!validationParameters.ValidateSignatureLast) + { + InternalValidators.ValidateAfterSignatureFailed( + jwtToken, + jwtToken.ValidFromNullable, + jwtToken.ValidToNullable, + jwtToken.Audiences, + validationParameters, + configuration); + } + } + + if (keysAttempted is not null) + { + if (kidExists) + { + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, + LogHelper.MarkAsNonPII(jwtToken.Kid), + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + (object)exceptionStrings ?? "", + jwtToken))); + } + else + { + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10517, + LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), + LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), + LogHelper.MarkAsNonPII(numKeysInConfiguration), + (object)exceptionStrings ?? "", + jwtToken))); + } + } + + throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); + } + + internal static bool IsSignatureValid(byte[] signatureBytes, int signatureBytesLength, SignatureProvider signatureProvider, byte[] dataToVerify, int dataToVerifyLength) + { + if (signatureProvider is SymmetricSignatureProvider) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + if (signatureBytes.Length == signatureBytesLength) + { + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); + } + else + { + byte[] sigBytes = new byte[signatureBytesLength]; + Array.Copy(signatureBytes, 0, sigBytes, 0, signatureBytesLength); + return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, sigBytes, 0, signatureBytesLength); + } + } + } + + internal static bool ValidateSignature(byte[] bytes, int len, string stringWithSignature, int signatureStartIndex, SignatureProvider signatureProvider) + { + return Base64UrlEncoding.Decode( + stringWithSignature, + signatureStartIndex + 1, + stringWithSignature.Length - signatureStartIndex - 1, + signatureProvider, + bytes, + len, + IsSignatureValid); + } + + internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) + { + var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; + if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(jsonWebToken.Alg), key); + + return false; + } + + Validators.ValidateAlgorithm(jsonWebToken.Alg, key, jsonWebToken, validationParameters); + var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); + try + { + if (signatureProvider == null) + throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(jsonWebToken.Alg)))); + + return EncodingUtils.PerformEncodingDependentOperation( + jsonWebToken.EncodedToken, + 0, + jsonWebToken.Dot2, + Encoding.UTF8, + jsonWebToken.EncodedToken, + jsonWebToken.Dot2, + signatureProvider, + ValidateSignature); + } + finally + { + cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); + } + } + + private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) + { + 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))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + else if (validationParameters.SignatureValidator != null) + { + var validatedToken = validationParameters.SignatureValidator(jsonWebToken.EncodedToken, validationParameters); + if (validatedToken == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + + if (!(validatedToken is JsonWebToken validatedJsonWebToken)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); + + return validatedJsonWebToken; + } + + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); + } + + /// + /// Validates a JWS or a JWE. + /// + /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. + /// A required for validation. + /// A + [Obsolete("`JsonWebTokens.ValidateToken(string, TokenValidationParameters)` has been deprecated and will be removed in a future release. Use `JsonWebTokens.ValidateTokenAsync(string, TokenValidationParameters)` instead. For more information, see https://aka.ms/IdentityModel/7-breaking-changes", false)] + public virtual TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) + { + return ValidateTokenAsync(token, validationParameters).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Validates a token. + /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. + /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. + /// + /// The token to be validated. + /// A required for validation. + /// A + /// + /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. + /// if is null or empty. + /// if is null. + /// 'token.Length' is greater than . + /// if is not a valid , + /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , + /// + public override async Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) + { + if (string.IsNullOrEmpty(token)) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + if (token.Length > MaximumTokenSizeInBytes) + return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; + + try + { + TokenValidationResult result = ReadToken(token, validationParameters); + if (result.IsValid) + return await ValidateTokenAsync(result.SecurityToken, validationParameters).ConfigureAwait(false); + + return result; + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + public override async Task ValidateTokenAsync(SecurityToken token, TokenValidationParameters validationParameters) + { + if (token == null) + throw LogHelper.LogArgumentNullException(nameof(token)); + + if (validationParameters == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; + + var jwt = token as JsonWebToken; + if (jwt == null) + return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; + + try + { + return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); + } + catch (Exception ex) + { + return new TokenValidationResult + { + Exception = ex, + IsValid = false + }; + } + } + + /// + /// Private method for token validation, responsible for: + /// (1) Obtaining a configuration from the . + /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. + /// + /// The JWT token + /// The to be used for validation. + /// + internal async ValueTask ValidateTokenAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters) + { + BaseConfiguration currentConfiguration = null; + if (validationParameters.ConfigurationManager != null) + { + try + { + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set + // directly on them, allowing the library to continue with token validation. + if (LogHelper.IsEnabled(EventLogLevel.Warning)) + LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); + } + } + + TokenValidationResult tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + + if (validationParameters.ConfigurationManager != null) + { + if (tokenValidationResult.IsValid) + { + // Set current configuration as LKG if it exists. + if (currentConfiguration != null) + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + + return tokenValidationResult; + } + else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception)) + { + // If we were still unable to validate, attempt to refresh the configuration and validate using it + // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on + // retrieval error as this case should have already been hit before. This refresh handles the case + // where a new valid configuration was somehow published during validation time. + if (currentConfiguration != null) + { + validationParameters.ConfigurationManager.RequestRefresh(); + validationParameters.RefreshBeforeValidation = true; + var lastConfig = currentConfiguration; + currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); + + // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. + if (lastConfig != currentConfiguration) + { + tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + { + validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; + return tokenValidationResult; + } + } + } + + if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) + { + validationParameters.RefreshBeforeValidation = false; + validationParameters.ValidateWithLKG = true; + var recoverableException = tokenValidationResult.Exception; + + foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations()) + { + if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException)) + { + tokenValidationResult = jsonWebToken.IsEncrypted ? + await ValidateJWEAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false) : + await ValidateJWSAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false); + + if (tokenValidationResult.IsValid) + return tokenValidationResult; + } + } + } + } + } + + return tokenValidationResult; + } + + internal async ValueTask ValidateTokenPayloadAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; + var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; + + Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); + Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); + string issuer = await Validators.ValidateIssuerAsync(jsonWebToken.Issuer, jsonWebToken, validationParameters, configuration).ConfigureAwait(false); + + Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string + // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, + // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature + // and (since issuer validation occurs first) came from a trusted authority. + // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, + // this code will still work properly. + TokenValidationResult tokenValidationResult = + await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + } + + string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) + { + IsValid = true, + TokenType = tokenType + }; + } + + internal async ValueTask ValidateTokenPayloadAsync( + JsonWebToken jsonWebToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; + var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; + + Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); + Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); + + IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( + jsonWebToken.Issuer, + jsonWebToken, + validationParameters, + callContext, + cancellationToken).ConfigureAwait(false); + + if (!issuerValidationResult.IsValid) + { + return new TokenValidationResult(jsonWebToken, this, validationParameters, issuerValidationResult.Issuer) + { + IsValid = false, + Exception = issuerValidationResult.Exception + }; + } + + Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); + if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) + { + // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string + // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, + // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature + // and (since issuer validation occurs first) came from a trusted authority. + // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, + // this code will still work properly. + TokenValidationResult tokenValidationResult = + await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); + + if (!tokenValidationResult.IsValid) + return tokenValidationResult; + } + + string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); + return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuerValidationResult.Issuer) + { + IsValid = true, + TokenType = tokenType + }; + } + } +} diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs index cf4ad2e264..43d1999a3c 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.cs @@ -3,12 +3,8 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Security.Claims; -using System.Text; using System.Text.RegularExpressions; -using System.Threading; -using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; using Microsoft.IdentityModel.Tokens; @@ -174,15 +170,6 @@ public virtual bool CanReadToken(string token) } } - /// - /// Returns a value that indicates if this handler can validate a . - /// - /// 'true', indicating this instance can validate a . - public virtual bool CanValidateToken - { - get { return true; } - } - private static StringComparison GetStringComparisonRuleIf509(SecurityKey securityKey) => (securityKey is X509SecurityKey) ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; @@ -498,90 +485,6 @@ public override SecurityToken ReadToken(string token) return ReadJsonWebToken(token); } - /// - /// Validates a JWS or a JWE. - /// - /// A 'JSON Web Token' (JWT) in JWS or JWE Compact Serialization Format. - /// A required for validation. - /// A - [Obsolete("`JsonWebTokens.ValidateToken(string, TokenValidationParameters)` has been deprecated and will be removed in a future release. Use `JsonWebTokens.ValidateTokenAsync(string, TokenValidationParameters)` instead. For more information, see https://aka.ms/IdentityModel/7-breaking-changes", false)] - public virtual TokenValidationResult ValidateToken(string token, TokenValidationParameters validationParameters) - { - return ValidateTokenAsync(token, validationParameters).ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Validates a token. - /// On a validation failure, no exception will be thrown; instead, the exception will be set in the returned TokenValidationResult.Exception property. - /// Callers should always check the TokenValidationResult.IsValid property to verify the validity of the result. - /// - /// The token to be validated. - /// A required for validation. - /// A - /// - /// TokenValidationResult.Exception will be set to one of the following exceptions if the is invalid. - /// if is null or empty. - /// if is null. - /// 'token.Length' is greater than . - /// if is not a valid , - /// if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid , - /// - public override async Task ValidateTokenAsync(string token, TokenValidationParameters validationParameters) - { - if (string.IsNullOrEmpty(token)) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(token)), IsValid = false }; - - if (validationParameters == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; - - if (token.Length > MaximumTokenSizeInBytes) - return new TokenValidationResult { Exception = LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(TokenLogMessages.IDX10209, LogHelper.MarkAsNonPII(token.Length), LogHelper.MarkAsNonPII(MaximumTokenSizeInBytes)))), IsValid = false }; - - try - { - TokenValidationResult result = ReadToken(token, validationParameters); - if (result.IsValid) - return await ValidateTokenAsync(result.SecurityToken, validationParameters).ConfigureAwait(false); - - return result; - } - catch (Exception ex) - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false - }; - } - } - - /// - public override async Task ValidateTokenAsync(SecurityToken token, TokenValidationParameters validationParameters) - { - if (token == null) - throw LogHelper.LogArgumentNullException(nameof(token)); - - if (validationParameters == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentNullException(nameof(validationParameters)), IsValid = false }; - - var jwt = token as JsonWebToken; - if (jwt == null) - return new TokenValidationResult { Exception = LogHelper.LogArgumentException(nameof(token), $"{nameof(token)} must be a {nameof(JsonWebToken)}."), IsValid = false }; - - try - { - return await ValidateTokenAsync(jwt, validationParameters).ConfigureAwait(false); - } - catch (Exception ex) - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false - }; - } - } - /// /// Converts a string into an instance of . /// @@ -627,453 +530,5 @@ private static TokenValidationResult ReadToken(string token, TokenValidationPara IsValid = true }; } - - /// - /// Private method for token validation, responsible for: - /// (1) Obtaining a configuration from the . - /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. - /// - /// The JWT token - /// The to be used for validation. - /// - private async ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters) - { - BaseConfiguration currentConfiguration = null; - if (validationParameters.ConfigurationManager != null) - { - try - { - currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - // The exception is not re-thrown as the TokenValidationParameters may have the issuer and signing key set - // directly on them, allowing the library to continue with token validation. - if (LogHelper.IsEnabled(EventLogLevel.Warning)) - LogHelper.LogWarning(LogHelper.FormatInvariant(TokenLogMessages.IDX10261, validationParameters.ConfigurationManager.MetadataAddress, ex.ToString())); - } - } - - TokenValidationResult tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); - if (validationParameters.ConfigurationManager != null) - { - if (tokenValidationResult.IsValid) - { - // Set current configuration as LKG if it exists. - if (currentConfiguration != null) - validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; - - return tokenValidationResult; - } - else if (TokenUtilities.IsRecoverableException(tokenValidationResult.Exception)) - { - // If we were still unable to validate, attempt to refresh the configuration and validate using it - // but ONLY if the currentConfiguration is not null. We want to avoid refreshing the configuration on - // retrieval error as this case should have already been hit before. This refresh handles the case - // where a new valid configuration was somehow published during validation time. - if (currentConfiguration != null) - { - validationParameters.ConfigurationManager.RequestRefresh(); - validationParameters.RefreshBeforeValidation = true; - var lastConfig = currentConfiguration; - currentConfiguration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(CancellationToken.None).ConfigureAwait(false); - - // Only try to re-validate using the newly obtained config if it doesn't reference equal the previously used configuration. - if (lastConfig != currentConfiguration) - { - tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, currentConfiguration).ConfigureAwait(false); - - if (tokenValidationResult.IsValid) - { - validationParameters.ConfigurationManager.LastKnownGoodConfiguration = currentConfiguration; - return tokenValidationResult; - } - } - } - - if (validationParameters.ConfigurationManager.UseLastKnownGoodConfiguration) - { - validationParameters.RefreshBeforeValidation = false; - validationParameters.ValidateWithLKG = true; - var recoverableException = tokenValidationResult.Exception; - - foreach (BaseConfiguration lkgConfiguration in validationParameters.ConfigurationManager.GetValidLkgConfigurations()) - { - if (!lkgConfiguration.Equals(currentConfiguration) && TokenUtilities.IsRecoverableConfiguration(jsonWebToken.Kid, currentConfiguration, lkgConfiguration, recoverableException)) - { - tokenValidationResult = await ValidateTokenAsync(jsonWebToken, validationParameters, lkgConfiguration).ConfigureAwait(false); - - if (tokenValidationResult.IsValid) - return tokenValidationResult; - } - } - } - } - } - - return tokenValidationResult; - } - - private ValueTask ValidateTokenAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - return jsonWebToken.IsEncrypted ? - ValidateJWEAsync(jsonWebToken, validationParameters, configuration) : - ValidateJWSAsync(jsonWebToken, validationParameters, configuration); - } - - private async ValueTask ValidateJWSAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - try - { - TokenValidationResult tokenValidationResult; - if (validationParameters.TransformBeforeSignatureValidation != null) - jsonWebToken = validationParameters.TransformBeforeSignatureValidation(jsonWebToken, validationParameters) as JsonWebToken; - - if (validationParameters.SignatureValidator != null || validationParameters.SignatureValidatorUsingConfiguration != null) - { - var validatedToken = ValidateSignatureUsingDelegates(jsonWebToken, validationParameters, configuration); - tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, validatedToken, validationParameters, configuration); - } - else - { - if (validationParameters.ValidateSignatureLast) - { - tokenValidationResult = await ValidateTokenPayloadAsync(jsonWebToken, validationParameters, configuration).ConfigureAwait(false); - if (tokenValidationResult.IsValid) - tokenValidationResult.SecurityToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); - } - else - { - var validatedToken = ValidateSignatureAndIssuerSecurityKey(jsonWebToken, validationParameters, configuration); - tokenValidationResult = await ValidateTokenPayloadAsync(validatedToken, validationParameters, configuration).ConfigureAwait(false); - } - } - - return tokenValidationResult; - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false, - TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jsonWebToken : null - }; - } - } - - private async ValueTask ValidateJWEAsync(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - try - { - TokenValidationResult tokenValidationResult = ReadToken(DecryptToken(jwtToken, validationParameters, configuration), validationParameters); - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - - tokenValidationResult = await ValidateJWSAsync(tokenValidationResult.SecurityToken as JsonWebToken, validationParameters, configuration).ConfigureAwait(false); - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - - jwtToken.InnerToken = tokenValidationResult.SecurityToken as JsonWebToken; - jwtToken.Payload = (tokenValidationResult.SecurityToken as JsonWebToken).Payload; - return new TokenValidationResult - { - SecurityToken = jwtToken, - ClaimsIdentityNoLocking = tokenValidationResult.ClaimsIdentityNoLocking, - IsValid = true, - TokenType = tokenValidationResult.TokenType - }; - } -#pragma warning disable CA1031 // Do not catch general exception types - catch (Exception ex) -#pragma warning restore CA1031 // Do not catch general exception types - { - return new TokenValidationResult - { - Exception = ex, - IsValid = false, - TokenOnFailedValidation = validationParameters.IncludeTokenOnFailedValidation ? jwtToken : null - }; - } - } - - private static JsonWebToken ValidateSignatureUsingDelegates(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - if (validationParameters.SignatureValidatorUsingConfiguration != null) - { - var validatedToken = validationParameters.SignatureValidatorUsingConfiguration(jsonWebToken.EncodedToken, validationParameters, configuration); - if (validatedToken == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - - if (!(validatedToken is JsonWebToken validatedJsonWebToken)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); - - return validatedJsonWebToken; - } - else if (validationParameters.SignatureValidator != null) - { - var validatedToken = validationParameters.SignatureValidator(jsonWebToken.EncodedToken, validationParameters); - if (validatedToken == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - - if (!(validatedToken is JsonWebToken validatedJsonWebToken)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10506, LogHelper.MarkAsNonPII(typeof(JsonWebToken)), LogHelper.MarkAsNonPII(validatedToken.GetType()), jsonWebToken))); - - return validatedJsonWebToken; - } - - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10505, jsonWebToken))); - } - - private static JsonWebToken ValidateSignatureAndIssuerSecurityKey(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - JsonWebToken validatedToken = ValidateSignature(jsonWebToken, validationParameters, configuration); - Validators.ValidateIssuerSecurityKey(validatedToken.SigningKey, jsonWebToken, validationParameters, configuration); - - return validatedToken; - } - - private async ValueTask ValidateTokenPayloadAsync(JsonWebToken jsonWebToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - var expires = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Exp) ? (DateTime?)jsonWebToken.ValidTo : null; - var notBefore = jsonWebToken.HasPayloadClaim(JwtRegisteredClaimNames.Nbf) ? (DateTime?)jsonWebToken.ValidFrom : null; - - Validators.ValidateLifetime(notBefore, expires, jsonWebToken, validationParameters); - Validators.ValidateAudience(jsonWebToken.Audiences, jsonWebToken, validationParameters); - string issuer = await Validators.ValidateIssuerAsync(jsonWebToken.Issuer, jsonWebToken, validationParameters, configuration).ConfigureAwait(false); - - Validators.ValidateTokenReplay(expires, jsonWebToken.EncodedToken, validationParameters); - if (validationParameters.ValidateActor && !string.IsNullOrWhiteSpace(jsonWebToken.Actor)) - { - // Infinite recursion should not occur here, as the JsonWebToken passed into this method is (1) constructed from a string - // AND (2) the signature is successfully validated on it. (1) implies that even if there are nested actor tokens, - // they must end at some point since they cannot reference one another. (2) means that the token has a valid signature - // and (since issuer validation occurs first) came from a trusted authority. - // NOTE: More than one nested actor token should not be considered a valid token, but if we somehow encounter one, - // this code will still work properly. - TokenValidationResult tokenValidationResult = - await ValidateTokenAsync(jsonWebToken.Actor, validationParameters.ActorValidationParameters ?? validationParameters).ConfigureAwait(false); - - if (!tokenValidationResult.IsValid) - return tokenValidationResult; - } - - string tokenType = Validators.ValidateTokenType(jsonWebToken.Typ, jsonWebToken, validationParameters); - return new TokenValidationResult(jsonWebToken, this, validationParameters.Clone(), issuer) - { - IsValid = true, - TokenType = tokenType - }; - } - - /// - /// Validates the JWT signature. - /// - private static JsonWebToken ValidateSignature(JsonWebToken jwtToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - bool kidMatched = false; - IEnumerable keys = null; - - if (!jwtToken.IsSigned) - { - if (validationParameters.RequireSignedTokens) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10504, jwtToken))); - else - return jwtToken; - } - - if (validationParameters.IssuerSigningKeyResolverUsingConfiguration != null) - { - keys = validationParameters.IssuerSigningKeyResolverUsingConfiguration(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters, configuration); - } - else if (validationParameters.IssuerSigningKeyResolver != null) - { - keys = validationParameters.IssuerSigningKeyResolver(jwtToken.EncodedToken, jwtToken, jwtToken.Kid, validationParameters); - } - else - { - var key = JwtTokenUtilities.ResolveTokenSigningKey(jwtToken.Kid, jwtToken.X5t, validationParameters, configuration); - if (key != null) - { - kidMatched = true; - keys = [key]; - } - } - - if (validationParameters.TryAllIssuerSigningKeys && keys.IsNullOrEmpty()) - { - // control gets here if: - // 1. User specified delegate: IssuerSigningKeyResolver returned null - // 2. ResolveIssuerSigningKey returned null - // Try all the keys. This is the degenerate case, not concerned about perf. - keys = TokenUtilities.GetAllSigningKeys(configuration, validationParameters); - } - - // keep track of exceptions thrown, keys that were tried - StringBuilder exceptionStrings = null; - StringBuilder keysAttempted = null; - var kidExists = !string.IsNullOrEmpty(jwtToken.Kid); - - if (keys != null) - { - foreach (var key in keys) - { - try - { - if (ValidateSignature(jwtToken, key, validationParameters)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(TokenLogMessages.IDX10242, jwtToken); - - jwtToken.SigningKey = key; - return jwtToken; - } - } - catch (Exception ex) - { - (exceptionStrings ??= new StringBuilder()).AppendLine(ex.ToString()); - } - - if (key != null) - { - (keysAttempted ??= new StringBuilder()).Append(key.ToString()).Append(" , KeyId: ").AppendLine(key.KeyId); - if (kidExists && !kidMatched && key.KeyId != null) - kidMatched = jwtToken.Kid.Equals(key.KeyId, key is X509SecurityKey ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal); - } - } - } - - // Get information on where keys used during token validation came from for debugging purposes. - var keysInTokenValidationParameters = TokenUtilities.GetAllSigningKeys(validationParameters: validationParameters); - var keysInConfiguration = TokenUtilities.GetAllSigningKeys(configuration); - var numKeysInTokenValidationParameters = keysInTokenValidationParameters.Count(); - var numKeysInConfiguration = keysInConfiguration.Count(); - - if (kidExists) - { - if (kidMatched) - { - JsonWebToken localJwtToken = jwtToken; // avoid closure on non-exceptional path - var isKidInTVP = keysInTokenValidationParameters.Any(x => x.KeyId.Equals(localJwtToken.Kid)); - var keyLocation = isKidInTVP ? "TokenValidationParameters" : "Configuration"; - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidSignatureException(LogHelper.FormatInvariant(TokenLogMessages.IDX10511, - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - LogHelper.MarkAsNonPII(keyLocation), - LogHelper.MarkAsNonPII(jwtToken.Kid), - (object)exceptionStrings ?? "", - jwtToken))); - } - - if (!validationParameters.ValidateSignatureLast) - { - InternalValidators.ValidateAfterSignatureFailed( - jwtToken, - jwtToken.ValidFromNullable, - jwtToken.ValidToNullable, - jwtToken.Audiences, - validationParameters, - configuration); - } - } - - if (keysAttempted is not null) - { - if (kidExists) - { - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10503, - LogHelper.MarkAsNonPII(jwtToken.Kid), - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - (object)exceptionStrings ?? "", - jwtToken))); - } - else - { - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(LogHelper.FormatInvariant(TokenLogMessages.IDX10517, - LogHelper.MarkAsNonPII((object)keysAttempted ?? ""), - LogHelper.MarkAsNonPII(numKeysInTokenValidationParameters), - LogHelper.MarkAsNonPII(numKeysInConfiguration), - (object)exceptionStrings ?? "", - jwtToken))); - } - } - - throw LogHelper.LogExceptionMessage(new SecurityTokenSignatureKeyNotFoundException(TokenLogMessages.IDX10500)); - } - - internal static bool IsSignatureValid(byte[] signatureBytes, int signatureBytesLength, SignatureProvider signatureProvider, byte[] dataToVerify, int dataToVerifyLength) - { - if (signatureProvider is SymmetricSignatureProvider) - { - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); - } - else - { - if (signatureBytes.Length == signatureBytesLength) - { - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, signatureBytes, 0, signatureBytesLength); - } - else - { - byte[] sigBytes = new byte[signatureBytesLength]; - Array.Copy(signatureBytes, 0, sigBytes, 0, signatureBytesLength); - return signatureProvider.Verify(dataToVerify, 0, dataToVerifyLength, sigBytes, 0, signatureBytesLength); - } - } - } - - internal static bool ValidateSignature(byte[] bytes, int len, string stringWithSignature, int signatureStartIndex, SignatureProvider signatureProvider) - { - return Base64UrlEncoding.Decode( - stringWithSignature, - signatureStartIndex + 1, - stringWithSignature.Length - signatureStartIndex - 1, - signatureProvider, - bytes, - len, - IsSignatureValid); - } - - internal static bool ValidateSignature(JsonWebToken jsonWebToken, SecurityKey key, TokenValidationParameters validationParameters) - { - var cryptoProviderFactory = validationParameters.CryptoProviderFactory ?? key.CryptoProviderFactory; - if (!cryptoProviderFactory.IsSupportedAlgorithm(jsonWebToken.Alg, key)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX14000, LogHelper.MarkAsNonPII(jsonWebToken.Alg), key); - - return false; - } - - Validators.ValidateAlgorithm(jsonWebToken.Alg, key, jsonWebToken, validationParameters); - var signatureProvider = cryptoProviderFactory.CreateForVerifying(key, jsonWebToken.Alg); - try - { - if (signatureProvider == null) - throw LogHelper.LogExceptionMessage(new InvalidOperationException(LogHelper.FormatInvariant(TokenLogMessages.IDX10636, key == null ? "Null" : key.ToString(), LogHelper.MarkAsNonPII(jsonWebToken.Alg)))); - - return EncodingUtils.PerformEncodingDependentOperation( - jsonWebToken.EncodedToken, - 0, - jsonWebToken.Dot2, - Encoding.UTF8, - jsonWebToken.EncodedToken, - jsonWebToken.Dot2, - signatureProvider, - ValidateSignature); - } - finally - { - cryptoProviderFactory.ReleaseSignatureProvider(signatureProvider); - } - } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs b/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs index f544c99e93..e346c7cf73 100644 --- a/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs +++ b/src/Microsoft.IdentityModel.Tokens/Exceptions/SecurityTokenException.cs @@ -2,18 +2,22 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Runtime.Serialization; +using System.Text; using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens { - /// /// Represents a security token exception. /// [Serializable] public class SecurityTokenException : Exception { + [NonSerialized] + private string _stackTrace; + /// /// Initializes a new instance of the class. /// @@ -55,6 +59,49 @@ protected SecurityTokenException(SerializationInfo info, StreamingContext contex { } + /// + /// Gets the stack trace that is captured when the exception is created. + /// + public override string StackTrace + { + get + { + if (_stackTrace == null) + { + if (ExceptionDetail == null) + return base.StackTrace; +#if NET8_0_OR_GREATER + _stackTrace = new StackTrace(ExceptionDetail.StackFrames).ToString(); +#else + StringBuilder sb = new(); + foreach (StackFrame frame in ExceptionDetail.StackFrames) + { + sb.Append(frame.ToString()); + sb.Append(Environment.NewLine); + } + + _stackTrace = sb.ToString(); +#endif + } + + return _stackTrace; + } + } + + /// + /// Gets or sets the source of the exception. + /// + public override string Source + { + get => base.Source; + set => base.Source = value; + } + + internal ExceptionDetail ExceptionDetail + { + get; set; + } + #if NET472 || NETSTANDARD2_0 || NET6_0_OR_GREATER /// /// When overridden in a derived class, sets the System.Runtime.Serialization.SerializationInfo diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index b9fe6c2996..679fbf2b8f 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -35,7 +35,7 @@ internal static class LogMessages public const string IDX10207 = "IDX10207: Unable to validate audience. The 'audiences' parameter is null."; public const string IDX10208 = "IDX10208: Unable to validate audience. validationParameters.ValidAudience is null or whitespace and validationParameters.ValidAudiences is null."; public const string IDX10209 = "IDX10209: Token has length: '{0}' which is larger than the MaximumTokenSizeInBytes: '{1}'."; - public const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace"; + public const string IDX10211 = "IDX10211: Unable to validate issuer. The 'issuer' parameter is null or whitespace."; public const string IDX10214 = "IDX10214: Audience validation failed. Audiences: '{0}'. Did not match: validationParameters.ValidAudience: '{1}' or validationParameters.ValidAudiences: '{2}'."; public const string IDX10222 = "IDX10222: Lifetime validation failed. The token is not yet valid. ValidFrom (UTC): '{0}', Current time (UTC): '{1}'."; public const string IDX10223 = "IDX10223: Lifetime validation failed. The token is expired. ValidTo (UTC): '{0}', Current time (UTC): '{1}'."; @@ -79,6 +79,8 @@ internal static class LogMessages //public const string IDX10263 = "IDX10263: Unable to re-validate with ConfigurationManager.LastKnownGoodConfiguration as it is expired."; public const string IDX10264 = "IDX10264: Reading issuer signing keys from validation parameters and configuration."; public const string IDX10265 = "IDX10265: Reading issuer signing keys from configuration."; + //public const string IDX10266 = "IDX10266: Unable to validate issuer. validationParameters.ValidIssuer is null or whitespace, validationParameters.ValidIssuers is null or empty and ConfigurationManager is null."; + // 10500 - SignatureValidation public const string IDX10500 = "IDX10500: Signature validation failed. No security keys were provided to validate the signature."; diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs index bb68764a6d..2112772699 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs +++ b/src/Microsoft.IdentityModel.Tokens/TokenValidationParameters.cs @@ -13,7 +13,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// Contains a set of parameters that are used by a when validating a . /// - public class TokenValidationParameters + public partial class TokenValidationParameters { private string _authenticationType; private TimeSpan _clockSkew = DefaultClockSkew; @@ -38,7 +38,7 @@ public class TokenValidationParameters /// Default for the maximum token size. /// /// 250 KB (kilobytes). - public const Int32 DefaultMaximumTokenSizeInBytes = 1024 * 250; + public const int DefaultMaximumTokenSizeInBytes = 1024 * 250; /// /// Copy constructor for . @@ -66,6 +66,7 @@ protected TokenValidationParameters(TokenValidationParameters other) IssuerSigningKeyValidatorUsingConfiguration = other.IssuerSigningKeyValidatorUsingConfiguration; IssuerValidator = other.IssuerValidator; IssuerValidatorAsync = other.IssuerValidatorAsync; + IssuerValidationDelegateAsync = other.IssuerValidationDelegateAsync; IssuerValidatorUsingConfiguration = other.IssuerValidatorUsingConfiguration; LifetimeValidator = other.LifetimeValidator; LogTokenId = other.LogTokenId; @@ -172,22 +173,6 @@ public string AuthenticationType } } - ///// - ///// Gets or sets the for validating X509Certificate2(s). - ///// - //public X509CertificateValidator CertificateValidator - //{ - // get - // { - // return _certificateValidator; - // } - - // set - // { - // _certificateValidator = value; - // } - //} - /// /// Gets or sets the clock skew to apply when validating a time. /// @@ -368,7 +353,6 @@ public virtual ClaimsIdentity CreateClaimsIdentity(SecurityToken securityToken, /// public IssuerValidator IssuerValidator { get; set; } - /// /// Gets or sets a delegate that will be used to validate the issuer of the token. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd b/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd new file mode 100644 index 0000000000..ebefb90859 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/AsyncValidate.cd @@ -0,0 +1,81 @@ + + + + + + ABEAIAABEEAAEAIAAAAAAAABEQAAAEEACABAAAAkIoA= + Validation\TokenValidationResult.cs + + + + + + AAEAgAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAA= + Validation\IssuerValidationResult.cs + + + + + + AAAEAAAAAAAAAAAAAAAAEAAEAAAAAAAAAEAABAAAAAA= + Validation\ExceptionDetail.cs + + + + + + + + + AIAAAAJAAAAAAAAAAAgAIAABAAgAAAAABEBBAAAAAAA= + Validation\ValidationResult.cs + + + + + + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEAAA= + Validation\LogDetail.cs + + + + + + + + + + + + AAAIAAAAAAAAAAAAAAIAAAQAAABAQAAAAAAAAAAAAAA= + Validation\ValidationFailureType.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAEIAAIAAAAAA= + Validation\MessageDetail.cs + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + CallContext.cs + + + + + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs new file mode 100644 index 0000000000..1ab311c9e8 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ExceptionDetail.cs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information so that Exceptions can be logged or thrown written as required. + /// + internal class ExceptionDetail + { + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the type of exception that occurred. + /// contains information about the stack frame where the exception occurred. + public ExceptionDetail(MessageDetail messageDetail, Type exceptionType, StackFrame stackFrame) + : this(messageDetail, exceptionType, stackFrame, null) + { + } + + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the type of exception that occurred. + /// contains information about the stack frame where the exception occurred. + /// is the inner exception that occurred. + public ExceptionDetail(MessageDetail messageDetail, Type exceptionType, StackFrame stackFrame, Exception innerException) + { + ExceptionType = exceptionType; + InnerException = innerException; + MessageDetail = messageDetail; + StackFrames.Add(stackFrame); + } + + /// + /// Creates an instance of an using + /// + /// An instantance of an Exception. + public Exception GetException() + { + if (InnerException != null) + return Activator.CreateInstance(ExceptionType, MessageDetail.Message, InnerException) as Exception; + + return Activator.CreateInstance(ExceptionType, MessageDetail.Message) as Exception; + } + + /// + /// Gets the type of exception that occurred. + /// + public Type ExceptionType { get; } + + /// + /// Gets the inner exception that occurred. + /// + public Exception InnerException { get; } + + /// + /// Gets the message details that are used to generate the exception message. + /// + public MessageDetail MessageDetail { get; } + + /// + /// Gets the stack frames where the exception occurred. + /// + public IList StackFrames { get; } = []; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd b/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd new file mode 100644 index 0000000000..cd69bb66db --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Exceptions.cd @@ -0,0 +1,35 @@ + + + + + + AIAAAAAAAgAAAgAAAAQAAAAAAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenException.cs + + + + + + + + + AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenValidationException.cs + + + + + + AAgAAEAAAAAAAAAAAAACAAAgAAAAAAAAAAAAAAAAAAA= + Exceptions\SecurityTokenInvalidIssuerException.cs + + + + + + AAAEAAAAAAAAAAAAAAAAEAAEAAAAAAAAAEAABAAAAAA= + Validation\ExceptionDetail.cs + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs new file mode 100644 index 0000000000..35c53dc561 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/IssuerValidationResult.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating a issuer. + /// The contains a collection of for each step in the token validation. + /// + internal class IssuerValidationResult : ValidationResult + { + private Exception _exception; + + /// + /// Creates an instance of + /// + /// is the issuer that was validated successfully. + public IssuerValidationResult(string issuer) + : base(ValidationFailureType.ValidationSucceeded) + { + Issuer = issuer; + IsValid = true; + } + + /// + /// Creates an instance of + /// + /// is the issuer that was intended to be validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public IssuerValidationResult(string issuer, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + Issuer = issuer; + IsValid = false; + } + + /// + /// Gets the that occurred during validation. + /// + public override Exception Exception + { + get + { + if (_exception != null || ExceptionDetail == null) + return _exception; + + HasValidOrExceptionWasRead = true; + _exception = ExceptionDetail.GetException(); + SecurityTokenInvalidIssuerException securityTokenInvalidIssuerException = _exception as SecurityTokenInvalidIssuerException; + if (securityTokenInvalidIssuerException != null) + { + securityTokenInvalidIssuerException.InvalidIssuer = Issuer; + securityTokenInvalidIssuerException.ExceptionDetail = ExceptionDetail; + securityTokenInvalidIssuerException.Source = "Microsoft.IdentityModel.Tokens"; + } + + return _exception; + } + } + + /// + /// Gets the issuer that was validated or intended to be validated. + /// + public string Issuer { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs new file mode 100644 index 0000000000..6b91a67c1e --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/LogDetail.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.IdentityModel.Abstractions; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information so that logs can be written when needed. + /// + internal class LogDetail + { + /// + /// Creates an instance of + /// + /// contains information about the exception that is used to generate the exception message. + /// is the level of the event log. + public LogDetail(MessageDetail messageDetail, EventLogLevel eventLogLevel) + { + EventLogLevel = eventLogLevel; + MessageDetail = messageDetail; + } + + /// + /// Gets the level of the event log. + /// + public EventLogLevel EventLogLevel { get; } + + /// + /// Gets the message detail. + /// + public MessageDetail MessageDetail { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs new file mode 100644 index 0000000000..1579ab14da --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains information about a message that is used to generate a message for logging or exceptions. + /// + internal class MessageDetail + { + private string _message; + + // TODO - remove the need to create NonPII objects, we could use tuples where bool == true => object is PII. + // TODO - does this need to be ReadOnlyMemory? + /// + /// Creates an instance of + /// + /// The message to be formated. + /// The parameters for formatting. + public MessageDetail(ReadOnlyMemory formatString, params object[] parameters) + { + // TODO - paramter validation. + FormatString = formatString; + Parameters = parameters; + } + + /// + /// Gets the formatted message. + /// + public string Message + { + get + { + _message ??= LogHelper.FormatInvariant(FormatString.ToString(), Parameters); + return _message; + } + } + + private ReadOnlyMemory FormatString { get; } + + private object[] Parameters { get; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs new file mode 100644 index 0000000000..cab11a6bbc --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationParameters.IssuerValidationDelegate.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// partial class for the IssuerValidation delegate. + /// + public partial class TokenValidationParameters + { + /// + /// Gets or sets a delegate that will be used to validate the issuer of a . + /// + internal IssuerValidationDelegateAsync IssuerValidationDelegateAsync { get; set; } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs similarity index 85% rename from src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs rename to src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs index 6714c71b9e..46c90738e0 100644 --- a/src/Microsoft.IdentityModel.Tokens/TokenValidationResult.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenValidationResult.cs @@ -19,10 +19,6 @@ public class TokenValidationResult private readonly TokenValidationParameters _validationParameters; private readonly TokenHandler _tokenHandler; - private Exception _exception; - private bool _hasIsValidOrExceptionBeenRead = false; - private bool _isValid = false; - // Fields lazily initialized in a thread-safe manner. _claimsIdentity is protected by the _claimsIdentitySyncObj // lock, and since null is a valid initialized value, _claimsIdentityInitialized tracks whether or not it's valid. // _claims is constructed by reading the data from the ClaimsIdentity and is synchronized using Interlockeds @@ -37,6 +33,11 @@ public class TokenValidationResult private ClaimsIdentity _claimsIdentity; private Dictionary _claims; private Dictionary _propertyBag; + // TODO - lazy creation of _validationResults + private List _validationResults = []; + + private Exception _exception; + private bool _isValid; /// /// Creates an instance of @@ -60,6 +61,18 @@ internal TokenValidationResult(SecurityToken securityToken, TokenHandler tokenHa SecurityToken = securityToken; } + /// + /// Adds a to the list of . + /// + /// the associated with one of the validation steps. For example . + internal void AddValidationResult(ValidationResult validationResult) + { + if (validationResult is null) + throw LogHelper.LogArgumentNullException(nameof(validationResult)); + + _validationResults.Add(validationResult); + } + /// /// The created from the validated security token. /// @@ -67,7 +80,7 @@ public IDictionary Claims { get { - if (!_hasIsValidOrExceptionBeenRead) + if (!HasValidOrExceptionWasRead) LogHelper.LogWarning(LogMessages.IDX10109); if (_claims is null && ClaimsIdentity is { } ci) @@ -162,7 +175,7 @@ public Exception Exception { get { - _hasIsValidOrExceptionBeenRead = true; + HasValidOrExceptionWasRead = true; return _exception; } set @@ -171,6 +184,8 @@ public Exception Exception } } + internal bool HasValidOrExceptionWasRead { get; set; } + /// /// Gets or sets the issuer that was found in the token. /// @@ -183,7 +198,7 @@ public bool IsValid { get { - _hasIsValidOrExceptionBeenRead = true; + HasValidOrExceptionWasRead = true; return _isValid; } set @@ -228,5 +243,19 @@ public bool IsValid /// (e.g for a JSON Web Token, from the "typ" header). /// public string TokenType { get; set; } + + /// + /// Gets the list of that contains the result of validating the token. + /// + internal IReadOnlyList ValidationResults + { + get + { + if (_validationResults is null) + Interlocked.CompareExchange(ref _validationResults, new List(), null); + + return _validationResults; + } + } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd new file mode 100644 index 0000000000..a6af5bfb82 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationDelegates.cd @@ -0,0 +1,21 @@ + + + + + + YAgEEAQAKMQck0AAi5R6AACRWgBkBQIAAQgYQsaIkxA= + Validation\TokenValidationParameters.IssuerValidationDelegate.cs + + + + + + + + + AAAAAAAAAACAAAAAACAQBAAAAAAAAAAAgAAAAAAAAAA= + Validation\Validators.Issuer.cs + + + + \ No newline at end of file diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs new file mode 100644 index 0000000000..3d872bb63a --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// The type of the failure that occurred when validating a . + /// + internal abstract class ValidationFailureType + { + /// + /// Creates an instance of + /// + protected ValidationFailureType(string name) + { + Name = name; + } + + /// + /// Gets the name of the . + /// + public string Name { get; } + + /// + /// Defines a type that represents a required parameter was null. + /// + public static readonly ValidationFailureType NullArgument = new NullArgumentFailure("NullArgument"); + private class NullArgumentFailure : ValidationFailureType { internal NullArgumentFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that issuer validation failed. + /// + public static readonly ValidationFailureType IssuerValidationFailed = new IssuerValidationFailure("IssuerValidationFailed"); + private class IssuerValidationFailure : ValidationFailureType { internal IssuerValidationFailure(string name) : base(name) { } } + + /// + /// Defines a type that represents that no evaluation has taken place. + /// + public static readonly ValidationFailureType ValidationNotEvaluated = new NotEvaluated("NotEvaluated"); + private class NotEvaluated : ValidationFailureType { internal NotEvaluated(string name) : base(name) { } } + + /// + /// Defines a type that represents that no evaluation has taken place. + /// + public static readonly ValidationFailureType ValidationSucceeded = new Succeeded("Succeeded"); + private class Succeeded : ValidationFailureType { internal Succeeded(string name) : base(name) { } } + + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs new file mode 100644 index 0000000000..f4d8ce0dc0 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationResult.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains results of a single step in validating a . + /// A maintains a list of for each step in the token validation. + /// + internal abstract class ValidationResult + { + private bool _isValid = false; + + /// + /// Creates an instance of + /// + protected ValidationResult() + { + ValidationFailureType = ValidationFailureType.ValidationNotEvaluated; + } + + /// + /// Creates an instance of + /// + /// The that occurred during validation. + protected ValidationResult(ValidationFailureType validationFailureType) + { + ValidationFailureType = validationFailureType; + } + + /// + /// Creates an instance of + /// + /// The that occurred during validation. + /// The representing the that occurred during validation. + protected ValidationResult(ValidationFailureType validationFailureType, ExceptionDetail exceptionDetail) + { + ValidationFailureType = validationFailureType; + ExceptionDetail = exceptionDetail; + } + + /// + /// Adds a new stack frame to the exception details. + /// + /// + public void AddStackFrame(StackFrame stackFrame) + { + ExceptionDetail.StackFrames.Add(stackFrame); + } + + /// + /// Gets the that occurred during validation. + /// + public abstract Exception Exception { get; } + + /// + /// Gets the that occurred during validation. + /// + public ExceptionDetail ExceptionDetail { get; } + + /// + /// True if the token was successfully validated, false otherwise. + /// + public bool IsValid + { + get + { + HasValidOrExceptionWasRead = true; + return _isValid; + } + set + { + _isValid = value; + } + } + + // TODO - HasValidOrExceptionWasRead, IsValid, Exception are temporary and will be removed when TokenValidationResult derives from ValidationResult. + /// + /// Gets or sets a boolean recording if IsValid or Exception was called. + /// + protected bool HasValidOrExceptionWasRead { get; set; } + + /// + /// Logs the validation result. + /// +#pragma warning disable CA1822 // Mark members as static + public void Log() +#pragma warning restore CA1822 // Mark members as static + { + // TODO - Do we need this, how will it work? + } + + /// + /// Contains any logs that would have been written. + /// + public IList LogDetails { get; } = new List(); + + /// + /// Gets the indicating why the validation was not satisfied. + /// + public ValidationFailureType ValidationFailureType + { + get; + } = ValidationFailureType.ValidationNotEvaluated; + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs new file mode 100644 index 0000000000..9c41082c53 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Audience.cs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Partial class for Audience Validation. + /// + public static partial class Validators + { + /// + /// Determines if the audiences found in a are valid. + /// + /// The audiences found in the . + /// The being validated. + /// required for validation. + /// If 'validationParameters' is null. + /// If 'audiences' is null and is true. + /// If is null or whitespace and is null. + /// If none of the 'audiences' matched either or one of . + /// An EXACT match is required. + public static void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.AudienceValidator != null) + { + if (!validationParameters.AudienceValidator(audiences, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage( + new SecurityTokenInvalidAudienceException( + LogHelper.FormatInvariant( + LogMessages.IDX10231, + LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString()))) + { + InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) + }); + + return; + } + + if (!validationParameters.ValidateAudience) + { + LogHelper.LogWarning(LogMessages.IDX10233); + return; + } + + if (audiences == null) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10207) { InvalidAudience = null }); + + if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10208) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); + + if (!audiences.Any()) + throw LogHelper.LogExceptionMessage( + new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) + { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); + + // create enumeration of all valid audiences from validationParameters + IEnumerable validationParametersAudiences; + + if (validationParameters.ValidAudiences == null) + validationParametersAudiences = new[] { validationParameters.ValidAudience }; + else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) + validationParametersAudiences = validationParameters.ValidAudiences; + else + validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); + + if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) + return; + + SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( + LogHelper.FormatInvariant(LogMessages.IDX10214, + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiences)), + LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences)))) + { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }; + + if (!validationParameters.LogValidationExceptions) + throw ex; + + throw LogHelper.LogExceptionMessage(ex); + } + + private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) + { + foreach (string tokenAudience in audiences) + { + if (string.IsNullOrWhiteSpace(tokenAudience)) + continue; + + foreach (string validAudience in validationParametersAudiences) + { + if (string.IsNullOrWhiteSpace(validAudience)) + continue; + + if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return true; + } + } + } + + return false; + } + + private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) + { + if (validAudience.Length == tokenAudience.Length) + { + if (string.Equals(validAudience, tokenAudience)) + return true; + } + else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) + return true; + + return false; + } + + private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, string validAudience) + { + int length = -1; + + if (validAudience.Length == tokenAudience.Length + 1 && validAudience.EndsWith("/", StringComparison.InvariantCulture)) + length = validAudience.Length - 1; + else if (tokenAudience.Length == validAudience.Length + 1 && tokenAudience.EndsWith("/", StringComparison.InvariantCulture)) + length = tokenAudience.Length - 1; + + // the length of the audiences is different by more than 1 and neither ends in a "/" + if (length == -1) + return false; + + if (string.CompareOrdinal(validAudience, 0, tokenAudience, 0, length) == 0) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); + + return true; + } + + return false; + } + + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs new file mode 100644 index 0000000000..32e6742c30 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -0,0 +1,293 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Definition for delegate that will validate the issuer value in a token. + /// + /// The issuer to validate. + /// The that is being validated. + /// required for validation. + /// + /// + /// A that contains the results of validating the issuer. + /// This delegate is not expected to throw. + internal delegate Task IssuerValidationDelegateAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken); + + /// + /// IssuerValidation + /// + public static partial class Validators + { + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If is null or whitespace and is null. + /// If 'issuer' failed to matched either or one of . + /// An EXACT match is required. + public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + return ValidateIssuer(issuer, securityToken, validationParameters, null); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If ' configuration' is null. + /// If is null or whitespace and is null and is null. + /// If 'issuer' failed to matched either or one of or . + /// An EXACT match is required. + internal static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) + { + ValueTask vt = ValidateIssuerAsync(issuer, securityToken, validationParameters, configuration); + return vt.IsCompletedSuccessfully ? + vt.Result : + vt.AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// The required for issuer and signing key validation. + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If ' configuration' is null. + /// If is null or whitespace and is null and is null. + /// If 'issuer' failed to matched either or one of or . + /// An EXACT match is required. + internal static async ValueTask ValidateIssuerAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + BaseConfiguration configuration) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.IssuerValidatorAsync != null) + return await validationParameters.IssuerValidatorAsync(issuer, securityToken, validationParameters).ConfigureAwait(false); + + if (validationParameters.IssuerValidatorUsingConfiguration != null) + return validationParameters.IssuerValidatorUsingConfiguration(issuer, securityToken, validationParameters, configuration); + + if (validationParameters.IssuerValidator != null) + return validationParameters.IssuerValidator(issuer, securityToken, validationParameters); + + if (!validationParameters.ValidateIssuer) + { + LogHelper.LogWarning(LogMessages.IDX10235); + return issuer; + } + + if (string.IsNullOrWhiteSpace(issuer)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10211) + { InvalidIssuer = issuer }); + + // Throw if all possible places to validate against are null or empty + if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) + && validationParameters.ValidIssuers.IsNullOrEmpty() + && string.IsNullOrWhiteSpace(configuration?.Issuer)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10204) + { InvalidIssuer = issuer }); + + if (configuration != null) + { + if (string.Equals(configuration.Issuer, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + } + + if (string.Equals(validationParameters.ValidIssuer, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + + if (validationParameters.ValidIssuers != null) + { + foreach (string str in validationParameters.ValidIssuers) + { + if (string.IsNullOrEmpty(str)) + { + LogHelper.LogInformation(LogMessages.IDX10262); + continue; + } + + if (string.Equals(str, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return issuer; + } + } + } + + SecurityTokenInvalidIssuerException ex = new SecurityTokenInvalidIssuerException( + LogHelper.FormatInvariant(LogMessages.IDX10205, + LogHelper.MarkAsNonPII(issuer), + LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), + LogHelper.MarkAsNonPII(configuration?.Issuer))) + { InvalidIssuer = issuer }; + + if (!validationParameters.LogValidationExceptions) + throw ex; + + throw LogHelper.LogExceptionMessage(ex); + } + + /// + /// Determines if an issuer found in a is valid. + /// + /// The issuer to validate + /// The that is being validated. + /// required for validation. + /// + /// + /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". + /// If 'validationParameters' is null. + /// If 'issuer' is null or whitespace and is true. + /// If is null or whitespace and is null. + /// If 'issuer' failed to matched either or one of . + /// An EXACT match is required. + internal static async Task ValidateIssuerAsync( + string issuer, + SecurityToken securityToken, + TokenValidationParameters validationParameters, + CallContext callContext, + CancellationToken cancellationToken) + { + if (string.IsNullOrWhiteSpace(issuer)) + { + return new IssuerValidationResult( + issuer, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true), + null)); + } + + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (securityToken == null) + throw LogHelper.LogArgumentNullException(nameof(securityToken)); + + BaseConfiguration configuration = null; + if (validationParameters.ConfigurationManager != null) + configuration = await validationParameters.ConfigurationManager.GetBaseConfigurationAsync(cancellationToken).ConfigureAwait(false); + + // Throw if all possible places to validate against are null or empty + if (string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) + && validationParameters.ValidIssuers.IsNullOrEmpty() + && string.IsNullOrWhiteSpace(configuration?.Issuer)) + { + return new IssuerValidationResult( + issuer, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + null), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))); + } + + // TODO - we should distinguish if configuration, TVP.ValidIssuer or TVP.ValidIssuers was used to validate the issuer. + if (configuration != null) + { + if (string.Equals(configuration.Issuer, issuer)) + { + // TODO - how and when to log + // Logs will have to be passed back to Wilson + // so that they can be written to the correct place and in the correct format respecting PII. + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer), callContext); + + return new IssuerValidationResult(issuer); + } + } + + if (string.Equals(validationParameters.ValidIssuer, issuer)) + { + return new IssuerValidationResult(issuer); + } + + if (validationParameters.ValidIssuers != null) + { + foreach (string str in validationParameters.ValidIssuers) + { + if (string.IsNullOrEmpty(str)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10262); + + continue; + } + + if (string.Equals(str, issuer)) + { + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); + + return new IssuerValidationResult(issuer); + } + } + } + + return new IssuerValidationResult( + issuer, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10205.AsMemory(), + LogHelper.MarkAsNonPII(issuer), + LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), + LogHelper.MarkAsNonPII(configuration?.Issuer)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs new file mode 100644 index 0000000000..77c553c4db --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs @@ -0,0 +1,50 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using Microsoft.IdentityModel.Logging; + +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// IssuerValidation + /// + public static partial class Validators + { + /// + /// Validates the lifetime of a . + /// + /// The 'notBefore' time found in the . + /// The 'expiration' time found in the . + /// The being validated. + /// required for validation. + /// If 'validationParameters' is null. + /// If 'expires.HasValue' is false and is true. + /// If 'notBefore' is > 'expires'. + /// If 'notBefore' is > DateTime.UtcNow. + /// If 'expires' is < DateTime.UtcNow. + /// All time comparisons apply . + public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + if (validationParameters == null) + throw LogHelper.LogArgumentNullException(nameof(validationParameters)); + + if (validationParameters.LifetimeValidator != null) + { + if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters)) + throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidLifetimeException(LogHelper.FormatInvariant(LogMessages.IDX10230, securityToken)) + { NotBefore = notBefore, Expires = expires }); + + return; + } + + if (!validationParameters.ValidateLifetime) + { + LogHelper.LogInformation(LogMessages.IDX10238); + return; + } + + ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters); + } + } +} diff --git a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs index 482f2f3d37..6c652f6a3b 100644 --- a/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs +++ b/src/Microsoft.IdentityModel.Tokens/ValidatorUtilities.cs @@ -3,6 +3,7 @@ using System; using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.Abstractions; namespace Microsoft.IdentityModel.Tokens { @@ -49,7 +50,8 @@ internal static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Se }); // if it reaches here, that means lifetime of the token is valid - LogHelper.LogInformation(LogMessages.IDX10239); + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + LogHelper.LogInformation(LogMessages.IDX10239); } } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validators.cs b/src/Microsoft.IdentityModel.Tokens/Validators.cs index 2c9f9e6ca1..000ba4c617 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validators.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validators.cs @@ -2,10 +2,8 @@ // Licensed under the MIT License. using System; -using System.Collections.Generic; using System.Linq; using System.Security.Cryptography.X509Certificates; -using System.Threading.Tasks; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; @@ -14,7 +12,7 @@ namespace Microsoft.IdentityModel.Tokens /// /// AudienceValidator /// - public static class Validators + public static partial class Validators { /// /// Validates if a given algorithm for a is valid. @@ -50,283 +48,6 @@ public static void ValidateAlgorithm(string algorithm, SecurityKey securityKey, } } - /// - /// Determines if the audiences found in a are valid. - /// - /// The audiences found in the . - /// The being validated. - /// required for validation. - /// If 'validationParameters' is null. - /// If 'audiences' is null and is true. - /// If is null or whitespace and is null. - /// If none of the 'audiences' matched either or one of . - /// An EXACT match is required. - public static void ValidateAudience(IEnumerable audiences, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.AudienceValidator != null) - { - if (!validationParameters.AudienceValidator(audiences, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage( - new SecurityTokenInvalidAudienceException( - LogHelper.FormatInvariant( - LogMessages.IDX10231, - LogHelper.MarkAsUnsafeSecurityArtifact(securityToken, t => t.ToString()))) - { - InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) - }); - - return; - } - - if (!validationParameters.ValidateAudience) - { - LogHelper.LogWarning(LogMessages.IDX10233); - return; - } - - if (audiences == null) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10207) { InvalidAudience = null }); - - if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience) && (validationParameters.ValidAudiences == null)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidAudienceException(LogMessages.IDX10208) { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - - if (!audiences.Any()) - throw LogHelper.LogExceptionMessage( - new SecurityTokenInvalidAudienceException(LogHelper.FormatInvariant(LogMessages.IDX10206)) - { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }); - - // create enumeration of all valid audiences from validationParameters - IEnumerable validationParametersAudiences; - - if (validationParameters.ValidAudiences == null) - validationParametersAudiences = new[] { validationParameters.ValidAudience }; - else if (string.IsNullOrWhiteSpace(validationParameters.ValidAudience)) - validationParametersAudiences = validationParameters.ValidAudiences; - else - validationParametersAudiences = validationParameters.ValidAudiences.Concat(new[] { validationParameters.ValidAudience }); - - if (AudienceIsValid(audiences, validationParameters, validationParametersAudiences)) - return; - - SecurityTokenInvalidAudienceException ex = new SecurityTokenInvalidAudienceException( - LogHelper.FormatInvariant(LogMessages.IDX10214, - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(audiences)), - LogHelper.MarkAsNonPII(validationParameters.ValidAudience ?? "null"), - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidAudiences)))) - { InvalidAudience = Utility.SerializeAsSingleCommaDelimitedString(audiences) }; - - if (!validationParameters.LogValidationExceptions) - throw ex; - - throw LogHelper.LogExceptionMessage(ex); - } - - private static bool AudienceIsValid(IEnumerable audiences, TokenValidationParameters validationParameters, IEnumerable validationParametersAudiences) - { - foreach (string tokenAudience in audiences) - { - if (string.IsNullOrWhiteSpace(tokenAudience)) - continue; - - foreach (string validAudience in validationParametersAudiences) - { - if (string.IsNullOrWhiteSpace(validAudience)) - continue; - - if (AudiencesMatch(validationParameters, tokenAudience, validAudience)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - - return true; - } - } - } - - return false; - } - - private static bool AudiencesMatch(TokenValidationParameters validationParameters, string tokenAudience, string validAudience) - { - if (validAudience.Length == tokenAudience.Length) - { - if (string.Equals(validAudience, tokenAudience)) - return true; - } - else if (validationParameters.IgnoreTrailingSlashWhenValidatingAudience && AudiencesMatchIgnoringTrailingSlash(tokenAudience, validAudience)) - return true; - - return false; - } - - private static bool AudiencesMatchIgnoringTrailingSlash(string tokenAudience, string validAudience) - { - int length = -1; - - if (validAudience.Length == tokenAudience.Length + 1 && validAudience.EndsWith("/", StringComparison.InvariantCulture)) - length = validAudience.Length - 1; - else if (tokenAudience.Length == validAudience.Length + 1 && tokenAudience.EndsWith("/", StringComparison.InvariantCulture)) - length = tokenAudience.Length - 1; - - // the length of the audiences is different by more than 1 and neither ends in a "/" - if (length == -1) - return false; - - if (string.CompareOrdinal(validAudience, 0, tokenAudience, 0, length) == 0) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10234, LogHelper.MarkAsNonPII(tokenAudience)); - - return true; - } - - return false; - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If is null or whitespace and is null. - /// If 'issuer' failed to matched either or one of . - /// An EXACT match is required. - public static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - return ValidateIssuer(issuer, securityToken, validationParameters, null); - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The required for issuer and signing key validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If ' configuration' is null. - /// If is null or whitespace and is null and is null. - /// If 'issuer' failed to matched either or one of or . - /// An EXACT match is required. - internal static string ValidateIssuer(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters, BaseConfiguration configuration) - { - ValueTask vt = ValidateIssuerAsync(issuer, securityToken, validationParameters, configuration); - return vt.IsCompletedSuccessfully ? - vt.Result : - vt.AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); - } - - /// - /// Determines if an issuer found in a is valid. - /// - /// The issuer to validate - /// The that is being validated. - /// required for validation. - /// The required for issuer and signing key validation. - /// The issuer to use when creating the "Claim"(s) in a "ClaimsIdentity". - /// If 'validationParameters' is null. - /// If 'issuer' is null or whitespace and is true. - /// If ' configuration' is null. - /// If is null or whitespace and is null and is null. - /// If 'issuer' failed to matched either or one of or . - /// An EXACT match is required. - internal static async ValueTask ValidateIssuerAsync( - string issuer, - SecurityToken securityToken, - TokenValidationParameters validationParameters, - BaseConfiguration configuration) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.IssuerValidatorAsync != null) - return await validationParameters.IssuerValidatorAsync(issuer, securityToken, validationParameters).ConfigureAwait(false); - - if (validationParameters.IssuerValidatorUsingConfiguration != null) - return validationParameters.IssuerValidatorUsingConfiguration(issuer, securityToken, validationParameters, configuration); - - if (validationParameters.IssuerValidator != null) - return validationParameters.IssuerValidator(issuer, securityToken, validationParameters); - - if (!validationParameters.ValidateIssuer) - { - LogHelper.LogWarning(LogMessages.IDX10235); - return issuer; - } - - if (string.IsNullOrWhiteSpace(issuer)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10211) - { InvalidIssuer = issuer }); - - // Throw if all possible places to validate against are null or empty - if ( string.IsNullOrWhiteSpace(validationParameters.ValidIssuer) - && validationParameters.ValidIssuers.IsNullOrEmpty() - && string.IsNullOrWhiteSpace(configuration?.Issuer)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidIssuerException(LogMessages.IDX10204) - { InvalidIssuer = issuer }); - - if (configuration != null) - { - if (string.Equals(configuration.Issuer, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - } - - if (string.Equals(validationParameters.ValidIssuer, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - - if (validationParameters.ValidIssuers != null) - { - foreach (string str in validationParameters.ValidIssuers) - { - if (string.IsNullOrEmpty(str)) - { - LogHelper.LogInformation(LogMessages.IDX10262); - continue; - } - - if (string.Equals(str, issuer)) - { - if (LogHelper.IsEnabled(EventLogLevel.Informational)) - LogHelper.LogInformation(LogMessages.IDX10236, LogHelper.MarkAsNonPII(issuer)); - - return issuer; - } - } - } - - SecurityTokenInvalidIssuerException ex = new SecurityTokenInvalidIssuerException( - LogHelper.FormatInvariant(LogMessages.IDX10205, - LogHelper.MarkAsNonPII(issuer), - LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), - LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), - LogHelper.MarkAsNonPII(configuration?.Issuer))) - { InvalidIssuer = issuer }; - - if (!validationParameters.LogValidationExceptions) - throw ex; - - throw LogHelper.LogExceptionMessage(ex); - } - /// /// Validates the that signed a . /// @@ -422,42 +143,6 @@ internal static void ValidateIssuerSigningKeyLifeTime(SecurityKey securityKey, T } } - /// - /// Validates the lifetime of a . - /// - /// The 'notBefore' time found in the . - /// The 'expiration' time found in the . - /// The being validated. - /// required for validation. - /// If 'validationParameters' is null. - /// If 'expires.HasValue' is false and is true. - /// If 'notBefore' is > 'expires'. - /// If 'notBefore' is > DateTime.UtcNow. - /// If 'expires' is < DateTime.UtcNow. - /// All time comparisons apply . - public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters) - { - if (validationParameters == null) - throw LogHelper.LogArgumentNullException(nameof(validationParameters)); - - if (validationParameters.LifetimeValidator != null) - { - if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters)) - throw LogHelper.LogExceptionMessage(new SecurityTokenInvalidLifetimeException(LogHelper.FormatInvariant(LogMessages.IDX10230, securityToken)) - { NotBefore = notBefore, Expires = expires }); - - return; - } - - if (!validationParameters.ValidateLifetime) - { - LogHelper.LogInformation(LogMessages.IDX10238); - return; - } - - ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters); - } - /// /// Validates if a token has been replayed. /// diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs index f7b1d7b843..bd041cac2f 100644 --- a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandlerTests.cs @@ -3523,7 +3523,7 @@ public static TheoryData ValidateJwsWithConfigTheoryData incorrectSigningKeysConfig.SigningKeys.Add(KeyingMaterial.X509SecurityKey2); theoryData.Add(new JwtTheoryData { - TestId = nameof(Default.AsymmetricJws) + "_" + "TVPInvalid" + "_" + "ConfigSigningKeysInvalid" + "_SignatureValidatorReturnsValidToken", + TestId = nameof(Default.AsymmetricJws) + "_TVPInvalid_ConfigSigningKeysInvalid_SignatureValidatorReturnsValidToken", Token = Default.AsymmetricJws, ValidationParameters = new TokenValidationParameters { diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 753b97efad..872a9e8cea 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -19,6 +19,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text.Json; +using System.Xml.Linq; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Protocols; using Microsoft.IdentityModel.Protocols.OpenIdConnect; @@ -60,6 +61,7 @@ public class IdentityComparer { typeof(IEnumerable).ToString(), AreX509DataEnumsEqual }, { typeof(int).ToString(), AreIntsEqual }, { typeof(IssuerSerial).ToString(), CompareAllPublicProperties }, + { typeof(IssuerValidationResult).ToString(), AreIssuerValidationResultsEqual }, { typeof(JArray).ToString(), AreJArraysEqual }, { typeof(JObject).ToString(), AreJObjectsEqual }, { typeof(JsonElement).ToString(), AreJsonElementsEqual }, @@ -542,6 +544,66 @@ public static bool AreEqual(object object1, object object2, CompareContext conte return context.Merge(localContext); } + public static bool AreIssuerValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreIssuerValidationResultsEqual( + object1 as IssuerValidationResult, + object2 as IssuerValidationResult, + "IssuerValidationResult1", + "IssuerValidationResult2", + null, + context); + } + + internal static bool AreIssuerValidationResultsEqual( + IssuerValidationResult issuerValidationResult1, + IssuerValidationResult issuerValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(issuerValidationResult1, issuerValidationResult2, localContext)) + return context.Merge(localContext); + + if (issuerValidationResult1.Issuer != issuerValidationResult2.Issuer) + localContext.Diffs.Add($"IssuerValidationResult1.Issuer: {issuerValidationResult1.Issuer} != IssuerValidationResult2.Issuer: {issuerValidationResult2.Issuer}"); + + // true => both are not null. + if (ContinueCheckingEquality(issuerValidationResult1.Exception, issuerValidationResult2.Exception, localContext)) + { + AreStringsEqual( + issuerValidationResult1.Exception.Message, + issuerValidationResult2.Exception.Message, + $"({name1})issuerValidationResult1.Exception.Message", + $"({name2})issuerValidationResult1.Exception.Message", + localContext); + + AreStringsEqual( + issuerValidationResult1.Exception.Source, + issuerValidationResult2.Exception.Source, + $"({name1})issuerValidationResult1.Exception.Source", + $"({name2})issuerValidationResult2.Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + issuerValidationResult1.Exception.StackTrace.Trim(), + issuerValidationResult2.Exception.StackTrace.Trim(), + $"({name1})issuerValidationResult1.Exception.StackTrace", + $"({name2})issuerValidationResult2.Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); @@ -1089,15 +1151,43 @@ public static bool AreStringsEqual(object object1, object object2, string name1, if (!string.Equals(str1, str2, context.StringComparison)) { - localContext.Diffs.Add($"{name1} != {name2}, StringComparison: '{context.StringComparison}'"); - localContext.Diffs.Add(str1); + localContext.Diffs.Add($"'{name1}' != '{name2}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{str1}'"); localContext.Diffs.Add($"!="); - localContext.Diffs.Add(str2); + localContext.Diffs.Add($"'{str2}'"); + } + + return context.Merge(localContext); + } + + public static bool AreStringPrefixesEqual( + string string1, + string string2, + string name1, + string name2, + string prefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(string1, string2, localContext)) + return context.Merge(localContext); + + if (!string1.StartsWith(prefix, context.StringComparison)) + { + localContext.Diffs.Add($"'{name1}': does not start with prefix: '{prefix}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{string1}'"); + } + + if (!string2.StartsWith(prefix, context.StringComparison)) + { + localContext.Diffs.Add($"'{name2}': does not start with prefix: '{prefix}', StringComparison: '{context.StringComparison}'"); + localContext.Diffs.Add($"'{string2}'"); } return context.Merge(localContext); } + public static bool AreStringEnumDictionariesEqual(IDictionary> dictionary1, IDictionary> dictionary2, CompareContext context) { var localContext = new CompareContext(context); @@ -1316,6 +1406,26 @@ public static bool CompareAllPublicProperties(object obj1, object obj2, CompareC return context.Merge($"CompareAllPublicProperties: {type}", localContext); } + public static bool IsOnlyOneObjectNull(object object1, object object2, CompareContext context) + { + if (object1 == null && object2 == null) + return false; + + if (object1 == null) + { + context.Diffs.Add(BuildStringDiff(object2.GetType().ToString(), object1, object2)); + return true; + } + + if (object2 == null) + { + context.Diffs.Add(BuildStringDiff(object1.GetType().ToString(), object1, object2)); + return true; + } + + return false; + } + public static bool ContinueCheckingEquality(object obj1, object obj2, CompareContext context) { if (obj1 == null && obj2 == null) diff --git a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs index f0663cc997..1170d40822 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ValidationDelegates.cs @@ -87,7 +87,7 @@ public static string IssuerValidatorUsingConfigEcho(string issuer, SecurityToken return issuer; } - public static ValueTask IssuerValidatorAsync(string issuer, SecurityToken token, TokenValidationParameters validationParameters) + public static ValueTask IssuerValidatorInternalAsync(string issuer, SecurityToken token, TokenValidationParameters validationParameters) { return new ValueTask(issuer); } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs index e78a0391b6..f293582ed3 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/IdentityComparerTests.cs @@ -575,9 +575,9 @@ public void CompareStrings() var string2 = "goodbye"; IdentityComparer.AreEqual(string1, string2, context); - Assert.True(context.Diffs.Count(s => s == "str1 != str2, StringComparison: 'Ordinal'") == 1); - Assert.True(context.Diffs[1] == string1); - Assert.True(context.Diffs[3] == string2); + Assert.True(context.Diffs.Count(s => s == "'str1' != 'str2', StringComparison: 'Ordinal'") == 1); + Assert.True(context.Diffs[1] == $"'{string1}'"); + Assert.True(context.Diffs[3] == $"'{string2}'"); } [Fact] diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs index dfbc058216..8dd4f023f1 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Json/JsonUtilities.cs @@ -7,6 +7,7 @@ using System.Text; using System.Text.Json; using Microsoft.IdentityModel.Protocols.OpenIdConnect; +using Microsoft.IdentityModel.JsonWebTokens; using Newtonsoft.Json.Linq; namespace Microsoft.IdentityModel.Tokens.Json.Tests @@ -169,6 +170,11 @@ public static void SetAdditionalDataValues(IDictionary dictionar dictionary["true"] = true; } + public static JsonWebToken CreateUnsignedJsonWebToken(string key, object value) + { + return new JsonWebToken(CreateUnsignedToken(key, value)); + } + public static string CreateUnsignedToken(string key, object value) { return EmptyHeader + "." + CreateEncodedJson(key, value) + "."; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs index 041de6d40d..a44e4c7152 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/TokenValidationParametersTests.cs @@ -15,7 +15,7 @@ namespace Microsoft.IdentityModel.Tokens.Tests { public class TokenValidationParametersTests { - int ExpectedPropertyCount = 59; + int ExpectedPropertyCount = 60; [Fact] public void Publics() @@ -71,6 +71,7 @@ public void Publics() IssuerSigningKey = issuerSigningKey, IssuerSigningKeyResolver = (token, securityToken, keyIdentifier, tvp) => { return new List { issuerSigningKey }; }, IssuerSigningKeys = issuerSigningKeys, + IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync, IssuerValidator = ValidationDelegates.IssuerValidatorEcho, LifetimeValidator = ValidationDelegates.LifetimeValidatorReturnsTrue, LogTokenId = true, @@ -290,8 +291,9 @@ private TokenValidationParameters CreateTokenValidationParameters() validationParameters.IssuerSigningKeyResolverUsingConfiguration = ValidationDelegates.IssuerSigningKeyResolverUsingConfiguration; validationParameters.IssuerSigningKeyValidator = ValidationDelegates.IssuerSigningKeyValidator; validationParameters.IssuerSigningKeyValidatorUsingConfiguration = ValidationDelegates.IssuerSigningKeyValidatorUsingConfiguration; + validationParameters.IssuerValidationDelegateAsync = Validators.ValidateIssuerAsync; validationParameters.IssuerValidator = ValidationDelegates.IssuerValidatorEcho; - validationParameters.IssuerValidatorAsync = ValidationDelegates.IssuerValidatorAsync; + validationParameters.IssuerValidatorAsync = ValidationDelegates.IssuerValidatorInternalAsync; validationParameters.IssuerValidatorUsingConfiguration = ValidationDelegates.IssuerValidatorUsingConfigEcho; validationParameters.LifetimeValidator = ValidationDelegates.LifetimeValidatorReturnsTrue; validationParameters.NameClaimTypeRetriever = ValidationDelegates.NameClaimTypeRetriever; diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs new file mode 100644 index 0000000000..f5bacfc0b7 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/AsyncValidatorsTests.cs @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.TestUtils; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class AsyncValidatorTests + { + [Theory, MemberData(nameof(AsyncIssuerValidatorTestCases))] + public async Task AsyncIssuerValidatorTests(IssuerValidatorTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.AsyncIssuerValidatorTests", theoryData); + try + { + IssuerValidationResult result = await Validators.ValidateIssuerAsync( + theoryData.Issuer, + theoryData.SecurityToken, + theoryData.ValidationParameters, + null, + CancellationToken.None).ConfigureAwait(false); + Exception exception = result.Exception; + context.Diffs.Add("Exception: " + exception.ToString()); + } + catch (Exception ex) + { + context.Diffs.Add("Exception: " + ex.ToString()); + } + } + + public static TheoryData AsyncIssuerValidatorTestCases + { + get + { + TheoryData theoryData = new TheoryData(); + + theoryData.Add(new IssuerValidatorTheoryData + { + Issuer = null, + ValidationParameters = new TokenValidationParameters(), + }); + + return theoryData; + } + } + } + + public class IssuerValidatorTheoryData : TheoryDataBase + { + public string Issuer { get; set; } + public TokenValidationParameters ValidationParameters { get; set; } + public SecurityToken SecurityToken { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs new file mode 100644 index 0000000000..3edb202206 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class IssuerValidationResultTests + { + [Theory, MemberData(nameof(IssuerValdationResultsTestCases), DisableDiscoveryEnumeration = true)] + public async Task IssuerValidatorAsyncTests(IssuerValidationResultsTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.IssuerValidatorAsyncTests", theoryData); + + try + { + IssuerValidationResult issuerValidationResult = await Validators.ValidateIssuerAsync( + theoryData.Issuer, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext(), + CancellationToken.None).ConfigureAwait(false); + + theoryData.ExpectedException.ProcessException(issuerValidationResult.Exception, context); + IdentityComparer.AreIssuerValidationResultsEqual( + issuerValidationResult, + theoryData.IssuerValidationResult, + context); + } + catch (SecurityTokenInvalidIssuerException ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData IssuerValdationResultsTestCases + { + get + { + TheoryData theoryData = new(); + + string validIssuer = Guid.NewGuid().ToString(); + string issClaim = Guid.NewGuid().ToString(); + theoryData.Add(new IssuerValidationResultsTheoryData("Invalid_Issuer") + { + ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10205:"), + Issuer = issClaim, + IssuerValidationResult = new IssuerValidationResult( + issClaim, + ValidationFailureType.IssuerValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10205.AsMemory(), + LogHelper.MarkAsNonPII(issClaim), + LogHelper.MarkAsNonPII(validIssuer), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), + LogHelper.MarkAsNonPII(null)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))), + IsValid = false, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters { ValidIssuer = validIssuer } + }); + + theoryData.Add(new IssuerValidationResultsTheoryData("NULL_Issuer") + { + ExpectedException = ExpectedException.SecurityTokenInvalidIssuerException("IDX10211:"), + IssuerValidationResult = new IssuerValidationResult( + null, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10211.AsMemory(), + LogHelper.MarkAsNonPII(null), + LogHelper.MarkAsNonPII(validIssuer), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), + LogHelper.MarkAsNonPII(null)), + typeof(SecurityTokenInvalidIssuerException), + new StackFrame(true))), + IsValid = false, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Iss, issClaim), + ValidationParameters = new TokenValidationParameters(), + }); + + return theoryData; + } + } + } + + public class IssuerValidationResultsTheoryData : TheoryDataBase + { + public IssuerValidationResultsTheoryData(string testId) : base(testId) + { + } + + public BaseConfiguration Configuration { get; set; } + + public string Issuer { get; set; } + + internal IssuerValidationResult IssuerValidationResult { get; set; } + + public bool IsValid { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + + internal ValidationFailureType ValidationFailureType { get; set; } + } +} diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs similarity index 99% rename from test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs rename to test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs index d7af19095e..181c05e926 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/ValidatorsTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidatorsTests.cs @@ -8,8 +8,6 @@ using Microsoft.IdentityModel.TestUtils; using Xunit; -#pragma warning disable CS3016 // Arrays as attribute arguments is not CLS-compliant - namespace Microsoft.IdentityModel.Tokens.Tests { public class ValidatorsTests @@ -510,5 +508,3 @@ public class AudienceValidationTheoryData : TheoryDataBase } } } - -#pragma warning restore CS3016 // Arrays as attribute arguments is not CLS-compliant From f455cd23716b1eeecea9718187ac0fb0344f52bd Mon Sep 17 00:00:00 2001 From: id4s Date: Wed, 12 Jun 2024 21:14:21 -0700 Subject: [PATCH 2/2] Addressed PR comments. --- .../JsonWebTokenHandler.ValidateToken.cs | 2 +- .../Validation/MessageDetail.cs | 6 +++--- .../Validation/Validators.Issuer.cs | 6 +++--- .../Validation/IssuerValidationResultTests.cs | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs index 524559e198..3242311e9a 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.cs @@ -584,7 +584,7 @@ public override async Task ValidateTokenAsync(SecurityTok } /// - /// Private method for token validation, responsible for: + /// Internal method for token validation, responsible for: /// (1) Obtaining a configuration from the . /// (2) Revalidating using the Last Known Good Configuration (if present), and obtaining a refreshed configuration (if necessary) and revalidating using it. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs index 1579ab14da..800126a725 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/MessageDetail.cs @@ -20,7 +20,7 @@ internal class MessageDetail /// /// The message to be formated. /// The parameters for formatting. - public MessageDetail(ReadOnlyMemory formatString, params object[] parameters) + public MessageDetail(string formatString, params object[] parameters) { // TODO - paramter validation. FormatString = formatString; @@ -34,12 +34,12 @@ public string Message { get { - _message ??= LogHelper.FormatInvariant(FormatString.ToString(), Parameters); + _message ??= LogHelper.FormatInvariant(FormatString, Parameters); return _message; } } - private ReadOnlyMemory FormatString { get; } + private string FormatString { get; } private object[] Parameters { get; } } diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs index 32e6742c30..faca7d71e5 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.Issuer.cs @@ -201,7 +201,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.NullArgument, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, null), typeof(SecurityTokenInvalidIssuerException), new StackFrame(true), @@ -228,7 +228,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, null), typeof(SecurityTokenInvalidIssuerException), new StackFrame(true))); @@ -281,7 +281,7 @@ internal static async Task ValidateIssuerAsync( ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10205.AsMemory(), + LogMessages.IDX10205, LogHelper.MarkAsNonPII(issuer), LogHelper.MarkAsNonPII(validationParameters.ValidIssuer ?? "null"), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidIssuers)), diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs index 3edb202206..3c3f403fa2 100644 --- a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/IssuerValidationResultTests.cs @@ -60,7 +60,7 @@ public static TheoryData IssuerValdationResul ValidationFailureType.IssuerValidationFailed, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10205.AsMemory(), + LogMessages.IDX10205, LogHelper.MarkAsNonPII(issClaim), LogHelper.MarkAsNonPII(validIssuer), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)), @@ -80,7 +80,7 @@ public static TheoryData IssuerValdationResul ValidationFailureType.NullArgument, new ExceptionDetail( new MessageDetail( - LogMessages.IDX10211.AsMemory(), + LogMessages.IDX10211, LogHelper.MarkAsNonPII(null), LogHelper.MarkAsNonPII(validIssuer), LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(null)),