From 1dd1e987e0def24b15de3b2935e3f271c08b3557 Mon Sep 17 00:00:00 2001 From: Franco Fung <38921563+FuPingFranco@users.noreply.github.com> Date: Tue, 9 Jul 2024 13:37:32 -0700 Subject: [PATCH] Token Type validation: Remove exceptions (#2688) * Remove throws when validating Token Type. * Clean-up * Addressing Feedback --------- Co-authored-by: Franco Fung --- .../LogMessages.cs | 1 + .../Validation/TokenTypeValidationResult.cs | 70 ++++++ .../Validation/ValidationFailureType.cs | 8 +- .../Validation/Validators.TokenType.cs | 118 ++++++++++ .../ExpectedException.cs | 5 + .../IdentityComparer.cs | 66 ++++++ .../TokenTypeValidationResultTests.cs | 216 ++++++++++++++++++ 7 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs create mode 100644 test/Microsoft.IdentityModel.Tokens.Tests/Validation/TokenTypeValidationResultTests.cs diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index c644c0936f..7caa28fc86 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -73,6 +73,7 @@ internal static class LogMessages public const string IDX10256 = "IDX10256: Unable to validate the token type. TokenValidationParameters.ValidTypes is set, but the 'typ' header claim is null or empty."; public const string IDX10257 = "IDX10257: Token type validation failed. Type: '{0}'. Did not match: validationParameters.TokenTypes: '{1}'."; public const string IDX10258 = "IDX10258: Token type validated. Type: '{0}'."; + public const string IDX10259 = "IDX10259: Unable to validate the token type, delegate threw an exception."; // public const string IDX10260 = "IDX10260:"; public const string IDX10261 = "IDX10261: Unable to retrieve configuration from authority: '{0}'. \nProceeding with token validation in case the relevant properties have been set manually on the TokenValidationParameters. Exception caught: \n {1}. See https://aka.ms/validate-using-configuration-manager for additional information."; public const string IDX10262 = "IDX10262: One of the issuers in TokenValidationParameters.ValidIssuers was null or an empty string. See https://aka.ms/wilson/tokenvalidation for details."; diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs new file mode 100644 index 0000000000..e098694e71 --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenTypeValidationResult.cs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + /// + /// Contains the result of validating the TokenType of a . + /// The contains a collection of for each step in the token validation. + /// + internal class TokenTypeValidationResult : ValidationResult + { + private Exception? _exception; + private const string TokenSource = "Microsoft.IdentityModel.Tokens"; + + /// + /// Creates an instance of . + /// + /// is the type against which the token was validated. + public TokenTypeValidationResult(string? type) + : base(ValidationFailureType.ValidationSucceeded) + { + Type = type; + IsValid = true; + } + + /// + /// Creates an instance of + /// + /// is the type against which the token was validated. + /// is the that occurred during validation. + /// is the that occurred during validation. + public TokenTypeValidationResult(string? type, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail) + : base(validationFailure, exceptionDetail) + { + Type = type; + 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(); + if (_exception is SecurityTokenInvalidTypeException securityTokenInvalidTypeException) + { + securityTokenInvalidTypeException.InvalidType = Type; + securityTokenInvalidTypeException.Source = TokenSource; + } + + return _exception; + } + } + + /// + /// Gets the security token type. + /// + public string? Type { get; } + + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index 3786766934..562045da77 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -39,6 +39,12 @@ private class IssuerValidationFailure : ValidationFailureType { internal IssuerV public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed"); private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } } + /// + /// Defines a type that represents that token type validation failed. + /// + public static readonly ValidationFailureType TokenTypeValidationFailed = new TokenTypeValidationFailure("TokenTypeValidationFailed"); + private class TokenTypeValidationFailure : ValidationFailureType { internal TokenTypeValidationFailure(string name) : base(name) { } } + /// /// Defines a type that represents that signing key validation failed. /// @@ -48,7 +54,7 @@ private class SigningKeyValidationFailure : ValidationFailureType { internal Sig /// /// Defines a type that represents that lifetime validation failed. /// - public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailure"); + public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailed"); private class LifetimeValidationFailure : ValidationFailureType { internal LifetimeValidationFailure(string name) : base(name) { } } /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs index 6fd744fd9d..76d26cc37f 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs @@ -2,10 +2,12 @@ // Licensed under the MIT License. using System; +using System.Diagnostics; using System.Linq; using Microsoft.IdentityModel.Abstractions; using Microsoft.IdentityModel.Logging; +#nullable enable namespace Microsoft.IdentityModel.Tokens { public static partial class Validators @@ -57,5 +59,121 @@ public static string ValidateTokenType(string type, SecurityToken securityToken, return type; } + + /// + /// Validates the type of the token. + /// + /// The token type or null if it couldn't be resolved (e.g from the 'typ' header for a JWT). + /// The that is being validated. + /// required for validation. + /// + /// If is null. + /// If is null. + /// If is null or whitespace and is not null. + /// If failed to match . + /// An EXACT match is required. (case sensitive) is used for comparing against . +#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging + internal static TokenTypeValidationResult ValidateTokenType(string? type, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext) +#pragma warning restore CA1801 // TODO: remove pragma disable once callContext is used for logging + { + if (securityToken == null) + { + return new TokenTypeValidationResult( + type, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(securityToken))), + typeof(ArgumentNullException), + new StackFrame(true))); + } + + if (validationParameters == null) + { + return new TokenTypeValidationResult( + type, + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII(nameof(validationParameters))), + typeof(ArgumentNullException), + new StackFrame(true))); + } + + if (validationParameters.TypeValidator == null && (validationParameters.ValidTypes == null || !validationParameters.ValidTypes.Any())) + { + LogHelper.LogVerbose(LogMessages.IDX10255); + return new TokenTypeValidationResult(type); + } + + if (validationParameters.TypeValidator != null) + { + return ValidateTokenTypeUsingDelegate(type, securityToken, validationParameters); + } + + // Note: don't return an invalid TokenTypeValidationResult for a null or empty token type when a user-defined delegate is set + // to allow it to extract the actual token type from a different location (e.g from the claims). + if (string.IsNullOrEmpty(type)) + { + return new TokenTypeValidationResult( + type, + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10256, + LogHelper.MarkAsNonPII(nameof(type))), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true))); + } + + if (!validationParameters.ValidTypes.Contains(type, StringComparer.Ordinal)) + { + return new TokenTypeValidationResult( + type, + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10257, + LogHelper.MarkAsNonPII(nameof(type)), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidTypes))), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true))); + } + + if (LogHelper.IsEnabled(EventLogLevel.Informational)) + { + LogHelper.LogInformation(LogMessages.IDX10258, LogHelper.MarkAsNonPII(type)); + } + + return new TokenTypeValidationResult(type); + } + + private static TokenTypeValidationResult ValidateTokenTypeUsingDelegate(string? type, SecurityToken securityToken, TokenValidationParameters validationParameters) + { + try + { + var validatedType = validationParameters.TypeValidator(type, securityToken, validationParameters); + return new TokenTypeValidationResult(validatedType); + } +#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 TokenTypeValidationResult( + type, + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10259, + LogHelper.MarkAsNonPII(nameof(validationParameters.TypeValidator)), + LogHelper.MarkAsNonPII(ex.Message)), + ex.GetType(), + new StackFrame(true), + ex)); + } + } } } +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs b/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs index ee536b44d6..ab3dfc5d48 100644 --- a/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs +++ b/test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs @@ -265,6 +265,11 @@ public static ExpectedException SecurityTokenInvalidSignatureException(string su return new ExpectedException(typeof(SecurityTokenInvalidSignatureException), substringExpected, innerTypeExpected); } + public static ExpectedException SecurityTokenInvalidTypeException(string substringExpected = null, Type innerTypeExpected = null) + { + return new ExpectedException(typeof(SecurityTokenInvalidTypeException), substringExpected, innerTypeExpected); + } + public static ExpectedException SecurityTokenNoExpirationException(string substringExpected = null, Type innerTypeExpected = null) { return new ExpectedException(typeof(SecurityTokenNoExpirationException), substringExpected, innerTypeExpected); diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs index 5ccc712b9b..593e79b12f 100644 --- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs +++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs @@ -873,6 +873,72 @@ internal static bool AreTokenReplayValidationResultsEqual( return context.Merge(localContext); } + public static bool AreTokenTypeValidationResultsEqual(object object1, object object2, CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(object1, object2, context)) + return context.Merge(localContext); + + return AreTokenTypeValidationResultsEqual( + object1 as TokenTypeValidationResult, + object2 as TokenTypeValidationResult, + "TokenTypeValidationResult1", + "TokenTypeValidationResult2", + null, + context); + } + + internal static bool AreTokenTypeValidationResultsEqual( + TokenTypeValidationResult tokenTypeValidationResult1, + TokenTypeValidationResult tokenTypeValidationResult2, + string name1, + string name2, + string stackPrefix, + CompareContext context) + { + var localContext = new CompareContext(context); + if (!ContinueCheckingEquality(tokenTypeValidationResult1, tokenTypeValidationResult2, localContext)) + return context.Merge(localContext); + + if (tokenTypeValidationResult1.Type != tokenTypeValidationResult2.Type) + localContext.Diffs.Add($"TokenTypeValidationResult1.Type: '{tokenTypeValidationResult1.Type}' != TokenTypeValidationResult2.ExpirationTime: '{tokenTypeValidationResult2.Type}'"); + + if (tokenTypeValidationResult1.IsValid != tokenTypeValidationResult2.IsValid) + localContext.Diffs.Add($"TokenTypeValidationResult1.IsValid: {tokenTypeValidationResult1.IsValid} != TokenTypeValidationResult2.IsValid: {tokenTypeValidationResult2.IsValid}"); + + if (tokenTypeValidationResult1.ValidationFailureType != tokenTypeValidationResult2.ValidationFailureType) + localContext.Diffs.Add($"TokenTypeValidationResult1.ValidationFailureType: {tokenTypeValidationResult1.ValidationFailureType} != TokenTypeValidationResult2.ValidationFailureType: {tokenTypeValidationResult2.ValidationFailureType}"); + + // true => both are not null. + if (ContinueCheckingEquality(tokenTypeValidationResult1.Exception, tokenTypeValidationResult2.Exception, localContext)) + { + AreStringsEqual( + tokenTypeValidationResult1.Exception.Message, + tokenTypeValidationResult2.Exception.Message, + $"({name1}).Exception.Message", + $"({name2}).Exception.Message", + localContext); + + AreStringsEqual( + tokenTypeValidationResult1.Exception.Source, + tokenTypeValidationResult2.Exception.Source, + $"({name1}).Exception.Source", + $"({name2}).Exception.Source", + localContext); + + if (!string.IsNullOrEmpty(stackPrefix)) + AreStringPrefixesEqual( + tokenTypeValidationResult1.Exception.StackTrace.Trim(), + tokenTypeValidationResult2.Exception.StackTrace.Trim(), + $"({name1}).Exception.StackTrace", + $"({name2}).Exception.StackTrace", + stackPrefix.Trim(), + localContext); + } + + return context.Merge(localContext); + } + public static bool AreJArraysEqual(object object1, object object2, CompareContext context) { var localContext = new CompareContext(context); diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/TokenTypeValidationResultTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/TokenTypeValidationResultTests.cs new file mode 100644 index 0000000000..08433ef3ac --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/TokenTypeValidationResultTests.cs @@ -0,0 +1,216 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens.Json.Tests; +using Microsoft.IdentityModel.JsonWebTokens; +using Microsoft.IdentityModel.Logging; +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Validation.Tests +{ + public class TokenTypeValidationResultTests + { + [Theory, MemberData(nameof(TokenTypeValidationTestCases), DisableDiscoveryEnumeration = true)] + public void ValidateTokenType(TokenTypeTheoryData theoryData) + { + CompareContext context = TestUtilities.WriteHeader($"{this}.TokenTypeValidationResultTests", theoryData); + + TokenTypeValidationResult tokenTypeValidationResult = Validators.ValidateTokenType( + theoryData.Type, + theoryData.SecurityToken, + theoryData.ValidationParameters, + new CallContext()); + + if (tokenTypeValidationResult.Exception != null) + theoryData.ExpectedException.ProcessException(tokenTypeValidationResult.Exception); + else + theoryData.ExpectedException.ProcessNoException(); + + IdentityComparer.AreTokenTypeValidationResultsEqual( + tokenTypeValidationResult, + theoryData.TokenTypeValidationResult, + context); + + TestUtilities.AssertFailIfErrors(context); + + } + + public static TheoryData TokenTypeValidationTestCases + { + get + { + String[] validTypesNoJwt = { "ID Token", "Refresh Token", "Access Token" }; + String[] validTypesWithJwt = { "ID Token", "Refresh Token", "Access Token", "JWT" }; + + return new TheoryData + { + new TokenTypeTheoryData + { + TestId = "Valid_DefaultTokenTypeValidation", + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = new TokenValidationParameters + { + ValidTypes = validTypesWithJwt + }, + TokenTypeValidationResult = new TokenTypeValidationResult("JWT") + }, + new TokenTypeTheoryData + { + TestId = "Invalid_SecurityTokenIsNull", + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + Type = "JWT", + SecurityToken = null, + ValidationParameters = null, + TokenTypeValidationResult = new TokenTypeValidationResult( + "JWT", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("securityToken")), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new TokenTypeTheoryData + { + TestId = "Invalid_ValidationParametersAreNull", + ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"), + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = null, + TokenTypeValidationResult = new TokenTypeValidationResult( + "JWT", + ValidationFailureType.NullArgument, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10000, + LogHelper.MarkAsNonPII("validationParameters")), + typeof(ArgumentNullException), + new StackFrame(true))) + }, + new TokenTypeTheoryData + { + TestId = "Valid_ValidateTokenTypeUsingDelegate", + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = new TokenValidationParameters + { + TypeValidator = (Type, SecurityToken, TokenValidationParameters) => "JWT" + }, + TokenTypeValidationResult = new TokenTypeValidationResult("JWT") + }, + new TokenTypeTheoryData + { + TestId = "Invalid_ValidateTokenTypeUsingDelegate", + ExpectedException = ExpectedException.SecurityTokenInvalidTypeException(substringExpected: "IDX10259:", innerTypeExpected: typeof(SecurityTokenInvalidTypeException)), + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = new TokenValidationParameters + { + TypeValidator = (Type, SecurityToken, TokenValidationParameters) => throw new SecurityTokenInvalidTypeException() + }, + TokenTypeValidationResult = new TokenTypeValidationResult( + "JWT", + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10259, + LogHelper.MarkAsNonPII("TypeValidator"), + LogHelper.MarkAsNonPII("Delegate message")), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true), + new SecurityTokenInvalidTypeException())) + }, + new TokenTypeTheoryData + { + TestId = "Valid_TokenValidationParametersTypeValidatorAndValidTypesAreNull", + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = new TokenValidationParameters + { + TypeValidator = null, + ValidTypes = null + }, + TokenTypeValidationResult = new TokenTypeValidationResult("JWT") + }, + new TokenTypeTheoryData + { + TestId = "Invalid_TokenTypeIsEmpty", + ExpectedException = ExpectedException.SecurityTokenInvalidTypeException("IDX10256:"), + Type = String.Empty, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, String.Empty), + ValidationParameters = new TokenValidationParameters + { + ValidTypes = validTypesNoJwt + }, + TokenTypeValidationResult = new TokenTypeValidationResult( + string.Empty, + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10256, + LogHelper.MarkAsNonPII("type")), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true))) + }, + new TokenTypeTheoryData + { + TestId = "Invalid_TokenTypeIsNull", + ExpectedException = ExpectedException.SecurityTokenInvalidTypeException("IDX10256:"), + Type = null, + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, null), + ValidationParameters = new TokenValidationParameters + { + ValidTypes = validTypesNoJwt + }, + TokenTypeValidationResult = new TokenTypeValidationResult( + null, + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10256, + LogHelper.MarkAsNonPII("type")), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true))) + }, + new TokenTypeTheoryData + { + TestId = "Invalid_TokenValidationParametersValidTypesDoesNotSupportType", + ExpectedException = ExpectedException.SecurityTokenInvalidTypeException("IDX10257:"), + Type = "JWT", + SecurityToken = JsonUtilities.CreateUnsignedJsonWebToken(JwtRegisteredClaimNames.Typ, "JWT"), + ValidationParameters = new TokenValidationParameters + { + ValidTypes = validTypesNoJwt + }, + TokenTypeValidationResult = new TokenTypeValidationResult( + "JWT", + ValidationFailureType.TokenTypeValidationFailed, + new ExceptionDetail( + new MessageDetail( + LogMessages.IDX10257, + LogHelper.MarkAsNonPII("type"), + LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validTypesNoJwt))), + typeof(SecurityTokenInvalidTypeException), + new StackFrame(true))) + } + }; + } + } + + public class TokenTypeTheoryData : TheoryDataBase + { + public string Type { get; set; } + + public SecurityToken SecurityToken { get; set; } + + public TokenValidationParameters ValidationParameters { get; set; } + + internal TokenTypeValidationResult TokenTypeValidationResult { get; set; } + } + } +}