diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs index ded8b16947..39b501df24 100644 --- a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs @@ -301,13 +301,27 @@ private async ValueTask> ValidateJWSAsync( ex); } - ValidationResult replayValidationResult = validationParameters.TokenReplayValidator( - expires, jsonWebToken.EncodedToken, validationParameters, callContext); + ValidationResult replayValidationResult; - if (!replayValidationResult.IsValid) + try { - StackFrame replayValidationFailureStackFrame = StackFrames.ReplayValidationFailed ??= new StackFrame(true); - return replayValidationResult.UnwrapError().AddStackFrame(replayValidationFailureStackFrame); + replayValidationResult = validationParameters.TokenReplayValidator( + expires, jsonWebToken.EncodedToken, validationParameters, callContext); + + if (!replayValidationResult.IsValid) + return replayValidationResult.UnwrapError().AddCurrentStackFrame(); + } +#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 TokenReplayValidationError( + new MessageDetail(TokenLogMessages.IDX10276), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expires, + ex); } ValidationResult? actorValidationResult = null; diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs index 2a501e2705..3ba8835e1f 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs @@ -90,14 +90,31 @@ internal async Task> ValidateTokenAsync( if (samlToken.Assertion.Conditions is not null) { - ValidationResult tokenReplayValidationResult = Validators.ValidateTokenReplay( - samlToken.Assertion.Conditions.NotOnOrAfter, - samlToken.Assertion.CanonicalString, - validationParameters, - callContext); + ValidationResult tokenReplayValidationResult; + + try + { + tokenReplayValidationResult = validationParameters.TokenReplayValidator( + samlToken.Assertion.Conditions.NotOnOrAfter, + samlToken.Assertion.CanonicalString, + validationParameters, + callContext); - if (!tokenReplayValidationResult.IsValid) - return tokenReplayValidationResult.UnwrapError().AddCurrentStackFrame(); + if (!tokenReplayValidationResult.IsValid) + return tokenReplayValidationResult.UnwrapError().AddCurrentStackFrame(); + } +#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 TokenReplayValidationError( + new MessageDetail(Tokens.LogMessages.IDX10276), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + samlToken.Assertion.Conditions.NotOnOrAfter, + ex); + } } ValidationResult signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext); diff --git a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs index 8804d4d6ad..4df576ab59 100644 --- a/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs +++ b/src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs @@ -94,14 +94,31 @@ internal async Task> ValidateTokenAsync( if (samlToken.Assertion.Conditions is not null) { - ValidationResult tokenReplayValidationResult = Validators.ValidateTokenReplay( - samlToken.Assertion.Conditions.NotOnOrAfter, - samlToken.Assertion.CanonicalString, - validationParameters, - callContext); + ValidationResult tokenReplayValidationResult; - if (!tokenReplayValidationResult.IsValid) - return tokenReplayValidationResult.UnwrapError().AddCurrentStackFrame(); + try + { + tokenReplayValidationResult = validationParameters.TokenReplayValidator( + samlToken.Assertion.Conditions.NotOnOrAfter, + samlToken.Assertion.CanonicalString, + validationParameters, + callContext); + + if (!tokenReplayValidationResult.IsValid) + return tokenReplayValidationResult.UnwrapError().AddCurrentStackFrame(); + } +#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 TokenReplayValidationError( + new MessageDetail(Tokens.LogMessages.IDX10276), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + samlToken.Assertion.Conditions.NotOnOrAfter, + ex); + } } var signatureValidationResult = ValidateSignature(samlToken, validationParameters, callContext); diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index cbfa1e30c6..517ca0ead9 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -5,6 +5,7 @@ const Microsoft.IdentityModel.Tokens.LogMessages.IDX10272 = "IDX10272: Signature const Microsoft.IdentityModel.Tokens.LogMessages.IDX10273 = "IDX10273: AlgorithmValidationDelegate threw an exception, see inner exception." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10274 = "IDX10274: IssuerSigningKeyValidationDelegate threw an exception, see inner exception." -> string const Microsoft.IdentityModel.Tokens.LogMessages.IDX10275 = "IDX10275: TokenTypeValidationDelegate threw an exception, see inner exception." -> string +const Microsoft.IdentityModel.Tokens.LogMessages.IDX10276 = "IDX10276: TokenReplayValidationDelegate threw an exception, see inner exception." -> string Microsoft.IdentityModel.Tokens.AlgorithmValidationError Microsoft.IdentityModel.Tokens.AlgorithmValidationError.AlgorithmValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidAlgorithm, System.Exception innerException = null) -> void Microsoft.IdentityModel.Tokens.AlgorithmValidationError.InvalidAlgorithm.get -> string @@ -28,6 +29,9 @@ Microsoft.IdentityModel.Tokens.SecurityTokenInvalidOperationException.SecurityTo Microsoft.IdentityModel.Tokens.SignatureValidationError Microsoft.IdentityModel.Tokens.SignatureValidationError.InnerValidationError.get -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Tokens.SignatureValidationError.SignatureValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, Microsoft.IdentityModel.Tokens.ValidationError innerValidationError = null, System.Exception innerException = null) -> void +Microsoft.IdentityModel.Tokens.TokenReplayValidationError +Microsoft.IdentityModel.Tokens.TokenReplayValidationError.ExpirationTime.get -> System.DateTime? +Microsoft.IdentityModel.Tokens.TokenReplayValidationError.TokenReplayValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, System.DateTime? expirationTime, System.Exception innerException = null) -> void Microsoft.IdentityModel.Tokens.TokenTypeValidationError Microsoft.IdentityModel.Tokens.TokenTypeValidationError.InvalidTokenType.get -> string Microsoft.IdentityModel.Tokens.TokenTypeValidationError.TokenTypeValidationError(Microsoft.IdentityModel.Tokens.MessageDetail messageDetail, Microsoft.IdentityModel.Tokens.ValidationFailureType validationFailureType, System.Type exceptionType, System.Diagnostics.StackFrame stackFrame, string invalidTokenType, System.Exception innerException = null) -> void @@ -43,8 +47,10 @@ Microsoft.IdentityModel.Tokens.ValidationResult.IsValid.get -> bool Microsoft.IdentityModel.Tokens.ValidationResult.Result.get -> TResult override Microsoft.IdentityModel.Tokens.AlgorithmValidationError.GetException() -> System.Exception override Microsoft.IdentityModel.Tokens.SignatureValidationError.GetException() -> System.Exception +override Microsoft.IdentityModel.Tokens.TokenReplayValidationError.GetException() -> System.Exception override Microsoft.IdentityModel.Tokens.TokenTypeValidationError.GetException() -> System.Exception static Microsoft.IdentityModel.Tokens.SignatureValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.SignatureValidationError +static Microsoft.IdentityModel.Tokens.TokenReplayValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.TokenReplayValidationError static Microsoft.IdentityModel.Tokens.TokenTypeValidationError.NullParameter(string parameterName, System.Diagnostics.StackFrame stackFrame) -> Microsoft.IdentityModel.Tokens.TokenTypeValidationError static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedString(System.Collections.Generic.IList strings) -> string static Microsoft.IdentityModel.Tokens.ValidationError.GetCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> System.Diagnostics.StackFrame @@ -57,5 +63,6 @@ static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAl static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenExceedsMaximumSize -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenIsNotSigned -> Microsoft.IdentityModel.Tokens.ValidationFailureType +static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenReplayValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.TokenTypeValidatorThrew -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.XmlValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs index 6a203b74e7..30ce49b65e 100644 --- a/src/Microsoft.IdentityModel.Tokens/LogMessages.cs +++ b/src/Microsoft.IdentityModel.Tokens/LogMessages.cs @@ -91,6 +91,7 @@ internal static class LogMessages public const string IDX10272 = "IDX10272: SignatureValidationDelegate threw an exception, see inner exception."; public const string IDX10273 = "IDX10273: AlgorithmValidationDelegate threw an exception, see inner exception."; public const string IDX10275 = "IDX10275: TokenTypeValidationDelegate threw an exception, see inner exception."; + public const string IDX10276 = "IDX10276: TokenReplayValidationDelegate threw an exception, see inner exception."; // 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/Validation/Results/Details/TokenReplayValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/TokenReplayValidationError.cs new file mode 100644 index 0000000000..02d2c53d4a --- /dev/null +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/TokenReplayValidationError.cs @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Diagnostics; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens +{ + internal class TokenReplayValidationError : ValidationError + { + internal TokenReplayValidationError( + MessageDetail messageDetail, + ValidationFailureType validationFailureType, + Type exceptionType, + StackFrame stackFrame, + DateTime? expirationTime, + Exception? innerException = null) + : base(messageDetail, validationFailureType, exceptionType, stackFrame, innerException) + { + ExpirationTime = expirationTime; + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(SecurityTokenReplayDetectedException)) + { + SecurityTokenReplayDetectedException exception = new(MessageDetail.Message, InnerException); + exception.SetValidationError(this); + + return exception; + } + else if (ExceptionType == typeof(SecurityTokenReplayAddFailedException)) + { + SecurityTokenReplayAddFailedException exception = new(MessageDetail.Message, InnerException); + exception.SetValidationError(this); + + return exception; + } + + return base.GetException(); + } + + internal static new TokenReplayValidationError NullParameter(string parameterName, StackFrame stackFrame) => new( + MessageDetail.NullParameter(parameterName), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + stackFrame, + null); + + protected DateTime? ExpirationTime { get; } + } +} +#nullable restore diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs index bcdab9cfbd..9ee917bdf9 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs @@ -135,7 +135,7 @@ private class XmlValidationFailure : ValidationFailureType { internal XmlValidat public static readonly ValidationFailureType AlgorithmValidatorThrew = new AlgorithmValidationFailure("AlgorithmValidatorThrew"); /// - /// Defines a type that represents that a token is invalid. + /// Defines a type that represents the fact that the issuer validation delegate threw an exception. /// public static readonly ValidationFailureType IssuerValidatorThrew = new IssuerValidatorFailure("IssuerValidatorThrew"); private class IssuerValidatorFailure : ValidationFailureType { internal IssuerValidatorFailure(string name) : base(name) { } } @@ -145,6 +145,11 @@ private class IssuerValidatorFailure : ValidationFailureType { internal IssuerVa /// public static readonly ValidationFailureType SignatureValidatorThrew = new SignatureValidationFailure("SignatureValidatorThrew"); + /// + /// Defines a type that represents the fact that the token replay validation delegate threw an exception. + /// + public static readonly ValidationFailureType TokenReplayValidatorThrew = new TokenReplayValidationFailure("TokenReplayValidatorThrew"); + /// /// Defines a type that represents the fact that the token type validation delegate threw an exception. /// diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs index 064a6cc491..3e8de4ffd9 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenReplay.cs @@ -2,7 +2,6 @@ // Licensed under the MIT License. using System; -using System.Diagnostics; namespace Microsoft.IdentityModel.Tokens { @@ -43,44 +42,47 @@ public static partial class Validators #pragma warning restore CA1801 // Review unused parameters { if (string.IsNullOrWhiteSpace(securityToken)) - return ValidationError.NullParameter( + return TokenReplayValidationError.NullParameter( nameof(securityToken), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); if (validationParameters == null) - return ValidationError.NullParameter( + return TokenReplayValidationError.NullParameter( nameof(validationParameters), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame()); // check if token if replay cache is set, then there must be an expiration time. if (validationParameters.TokenReplayCache != null) { if (expirationTime == null) - return new ValidationError( + return new TokenReplayValidationError( new MessageDetail( LogMessages.IDX10227, securityToken), ValidationFailureType.TokenReplayValidationFailed, typeof(SecurityTokenNoExpirationException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame(), + expirationTime); if (validationParameters.TokenReplayCache.TryFind(securityToken)) - return new ValidationError( + return new TokenReplayValidationError( new MessageDetail( LogMessages.IDX10228, securityToken), ValidationFailureType.TokenReplayValidationFailed, typeof(SecurityTokenReplayDetectedException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame(), + expirationTime); if (!validationParameters.TokenReplayCache.TryAdd(securityToken, expirationTime.Value)) - return new ValidationError( + return new TokenReplayValidationError( new MessageDetail( LogMessages.IDX10229, securityToken), ValidationFailureType.TokenReplayValidationFailed, typeof(SecurityTokenReplayAddFailedException), - new StackFrame(true)); + ValidationError.GetCurrentStackFrame(), + expirationTime); } // if it reaches here, that means no token replay is detected. diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Extensibility.TokenReplay.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Extensibility.TokenReplay.cs new file mode 100644 index 0000000000..3f96081d85 --- /dev/null +++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.Extensibility.TokenReplay.cs @@ -0,0 +1,277 @@ +// 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.Tests; +using Microsoft.IdentityModel.Logging; +using Microsoft.IdentityModel.TestUtils; +using Microsoft.IdentityModel.Tokens; +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.JsonWebTokens.Extensibility.Tests +{ + public partial class JsonWebTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(TokenReplay_ExtensibilityTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_TokenReplayValidator_Extensibility(TokenReplayExtensibilityTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.{nameof(ValidateTokenAsync_TokenReplayValidator_Extensibility)}", theoryData); + context.IgnoreType = false; + for (int i = 0; i < theoryData.ExtraStackFrames; i++) + theoryData.TokenReplayValidationError!.AddStackFrame(new StackFrame(false)); + + try + { + ValidationResult validationResult = await theoryData.JsonWebTokenHandler.ValidateTokenAsync( + theoryData.JsonWebToken!, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + if (validationResult.IsValid) + { + context.Diffs.Add("validationResult.IsValid is true, expected false"); + } + else + { + ValidationError validationError = validationResult.UnwrapError(); + IdentityComparer.AreValidationErrorsEqual(validationError, theoryData.TokenReplayValidationError, context); + theoryData.ExpectedException.ProcessException(validationError.GetException(), context); + } + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData TokenReplay_ExtensibilityTestCases + { + get + { + var theoryData = new TheoryData(); + CallContext callContext = new CallContext(); + var utcNow = DateTime.UtcNow; + var expirationTime = utcNow + TimeSpan.FromHours(1); + + #region return CustomTokenReplayValidationError + // Test cases where delegate is overridden and return a CustomTokenReplayValidationError + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate, + extraStackFrames: 2) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 160), + expirationTime) + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate, + extraStackFrames: 2) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 175), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorUnknownExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate, + extraStackFrames: 2) + { + // CustomTokenReplayValidationError does not handle the exception type 'NotSupportedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(NotSupportedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate))), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(NotSupportedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 205), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException, ValidationFailureType: CustomAudienceValidationFailureType + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate, + extraStackFrames: 2) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate), null), + CustomTokenReplayValidationError.CustomTokenReplayValidationFailureType, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 190), + expirationTime), + }); + #endregion + + #region return TokenReplayValidationError + // Test cases where delegate is overridden and return an TokenReplayValidationError + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate, + extraStackFrames: 2) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 235), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate, + extraStackFrames: 2) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenReplayDetectedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 259), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate, + extraStackFrames: 2) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 274), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException, inner: CustomSecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorThrows", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + string.Format(Tokens.LogMessages.IDX10276), + typeof(CustomSecurityTokenReplayDetectedException)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + string.Format(Tokens.LogMessages.IDX10276), null), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("JsonWebTokenHandler.ValidateToken.Internal.cs", 250), + expirationTime, + new SecurityTokenReplayDetectedException(nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows)) + ) + }); + #endregion + + return theoryData; + } + } + + public class TokenReplayExtensibilityTheoryData : ValidateTokenAsyncBaseTheoryData + { + internal TokenReplayExtensibilityTheoryData(string testId, DateTime utcNow, TokenReplayValidationDelegate tokenReplayValidator, int extraStackFrames) : base(testId) + { + JsonWebToken = JsonWebTokenHandler.ReadJsonWebToken( + JsonWebTokenHandler.CreateToken( + new SecurityTokenDescriptor() + { + IssuedAt = utcNow, + NotBefore = utcNow, + Expires = utcNow + TimeSpan.FromHours(1), + })); + + ValidationParameters = new ValidationParameters + { + AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation, + AudienceValidator = SkipValidationDelegates.SkipAudienceValidation, + IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation, + IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation, + LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation, + SignatureValidator = SkipValidationDelegates.SkipSignatureValidation, + TokenReplayValidator = tokenReplayValidator, + TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation + }; + + ExtraStackFrames = extraStackFrames; + } + + public JsonWebToken JsonWebToken { get; } + + public JsonWebTokenHandler JsonWebTokenHandler { get; } = new JsonWebTokenHandler(); + + public bool IsValid { get; set; } + + internal TokenReplayValidationError? TokenReplayValidationError { get; set; } + + internal int ExtraStackFrames { get; } + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomTokenReplayValidationDelegates.cs b/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomTokenReplayValidationDelegates.cs new file mode 100644 index 0000000000..6e10f6cba8 --- /dev/null +++ b/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomTokenReplayValidationDelegates.cs @@ -0,0 +1,135 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Threading.Tasks; +using Microsoft.IdentityModel.Tokens; + +#nullable enable +namespace Microsoft.IdentityModel.TestUtils +{ + internal class CustomTokenReplayValidationDelegates + { + internal static ValidationResult CustomTokenReplayValidationDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + // Returns a CustomTokenReplayValidationError : IssuerValidationError + return new CustomTokenReplayValidationError( + new MessageDetail(nameof(CustomTokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + + internal static ValidationResult CustomTokenReplayValidatorCustomExceptionDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new CustomTokenReplayValidationError( + new MessageDetail(nameof(CustomTokenReplayValidatorCustomExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + + internal static ValidationResult CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new CustomTokenReplayValidationError( + new MessageDetail(nameof(CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate), null), + CustomTokenReplayValidationError.CustomTokenReplayValidationFailureType, + typeof(CustomSecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime, + null); + } + + internal static ValidationResult CustomTokenReplayValidatorUnknownExceptionDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new CustomTokenReplayValidationError( + new MessageDetail(nameof(CustomTokenReplayValidatorUnknownExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(NotSupportedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + + internal static ValidationResult CustomTokenReplayValidatorWithoutGetExceptionOverrideDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new CustomTokenReplayWithoutGetExceptionValidationOverrideError( + new MessageDetail(nameof(CustomTokenReplayValidatorWithoutGetExceptionOverrideDelegate), null), + typeof(CustomSecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + + internal static ValidationResult TokenReplayValidationDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new TokenReplayValidationError( + new MessageDetail(nameof(TokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + + internal static ValidationResult TokenReplayValidatorThrows( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + throw new CustomSecurityTokenReplayDetectedException(nameof(TokenReplayValidatorThrows), null); + } + + internal static ValidationResult TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new TokenReplayValidationError( + new MessageDetail(nameof(TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + internal static ValidationResult TokenReplayValidatorCustomExceptionTypeDelegate( + DateTime? expirationTime, + string securityToken, + ValidationParameters validationParameters, + CallContext callContext) + { + return new TokenReplayValidationError( + new MessageDetail(nameof(TokenReplayValidatorCustomExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenException), + ValidationError.GetCurrentStackFrame(), + expirationTime); + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomValidationErrors.cs b/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomValidationErrors.cs index 9623161345..af7361fa0b 100644 --- a/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomValidationErrors.cs +++ b/test/Microsoft.IdentityModel.TestUtils/TokenValidationExtensibility/CustomValidationErrors.cs @@ -293,6 +293,54 @@ public CustomAlgorithmWithoutGetExceptionValidationOverrideError( } #endregion // AlgorithmValidationErrors + #region TokenReplayValidationErrors + internal class CustomTokenReplayValidationError : TokenReplayValidationError + { + /// + /// A custom validation failure type. + /// + public static readonly ValidationFailureType CustomTokenReplayValidationFailureType = new TokenReplayValidationFailure("CustomTokenReplayValidationFailureType"); + private class TokenReplayValidationFailure : ValidationFailureType { internal TokenReplayValidationFailure(string name) : base(name) { } } + + public CustomTokenReplayValidationError( + MessageDetail messageDetail, + ValidationFailureType validationFailureType, + Type exceptionType, + StackFrame stackFrame, + DateTime? expirationTime, + Exception? innerException = null) + : base(messageDetail, validationFailureType, exceptionType, stackFrame, expirationTime, innerException) + { + } + + internal override Exception GetException() + { + if (ExceptionType == typeof(CustomSecurityTokenReplayDetectedException)) + { + var exception = new CustomSecurityTokenReplayDetectedException(MessageDetail.Message, InnerException); + exception.SetValidationError(this); + + return exception; + } + + return base.GetException(); + } + } + + internal class CustomTokenReplayWithoutGetExceptionValidationOverrideError : TokenReplayValidationError + { + public CustomTokenReplayWithoutGetExceptionValidationOverrideError( + MessageDetail messageDetail, + Type exceptionType, + StackFrame stackFrame, + DateTime? expirationTime, + Exception? innerException = null) + : base(messageDetail, ValidationFailureType.TokenReplayValidationFailed, exceptionType, stackFrame, expirationTime, innerException) + { + } + } + #endregion + // Other custom validation errors to be added here for signature validation, issuer signing key, etc. } #nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandler.Extensibility.TokenReplay.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandler.Extensibility.TokenReplay.cs new file mode 100644 index 0000000000..3266a4595a --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/Saml2SecurityTokenHandler.Extensibility.TokenReplay.cs @@ -0,0 +1,279 @@ +// 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.Logging; +using Microsoft.IdentityModel.TestUtils; + +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml2.Extensibility.Tests +{ + public partial class Saml2SecurityTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(TokenReplay_ExtensibilityTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_TokenReplayValidator_Extensibility(TokenReplayExtensibilityTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.{nameof(ValidateTokenAsync_TokenReplayValidator_Extensibility)}", theoryData); + context.IgnoreType = false; + for (int i = 0; i < theoryData.ExtraStackFrames; i++) + theoryData.TokenReplayValidationError!.AddStackFrame(new StackFrame(false)); + + try + { + ValidationResult validationResult = await theoryData.Saml2SecurityTokenHandler.ValidateTokenAsync( + theoryData.Saml2Token!, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + if (validationResult.IsValid) + { + context.Diffs.Add("validationResult.IsValid is true, expected false"); + } + else + { + ValidationError validationError = validationResult.UnwrapError(); + IdentityComparer.AreValidationErrorsEqual(validationError, theoryData.TokenReplayValidationError, context); + theoryData.ExpectedException.ProcessException(validationError.GetException(), context); + } + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData TokenReplay_ExtensibilityTestCases + { + get + { + var theoryData = new TheoryData(); + CallContext callContext = new CallContext(); + var utcNow = DateTime.UtcNow; + var expirationTime = utcNow + TimeSpan.FromHours(1); + + #region return CustomTokenReplayValidationError + // Test cases where delegate is overridden and return a CustomTokenReplayValidationError + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 160), + expirationTime) + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 175), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorUnknownExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate, + extraStackFrames: 1) + { + // CustomTokenReplayValidationError does not handle the exception type 'NotSupportedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(NotSupportedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate))), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(NotSupportedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 205), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException, ValidationFailureType: CustomAudienceValidationFailureType + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate), null), + CustomTokenReplayValidationError.CustomTokenReplayValidationFailureType, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 190), + expirationTime), + }); + #endregion + + #region return TokenReplayValidationError + // Test cases where delegate is overridden and return an TokenReplayValidationError + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 235), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate, + extraStackFrames: 1) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenReplayDetectedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 259), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate, + extraStackFrames: 1) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 274), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException, inner: CustomSecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorThrows", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows, + extraStackFrames: 0) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + string.Format(Tokens.LogMessages.IDX10276), + typeof(CustomSecurityTokenReplayDetectedException)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + string.Format(Tokens.LogMessages.IDX10276), null), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("Saml2SecurityTokenHandler.ValidateToken.Internal.cs", 250), + expirationTime, + new SecurityTokenReplayDetectedException(nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows)) + ) + }); + #endregion + + return theoryData; + } + } + + public class TokenReplayExtensibilityTheoryData : TheoryDataBase + { + internal TokenReplayExtensibilityTheoryData(string testId, DateTime utcNow, TokenReplayValidationDelegate tokenReplayValidator, int extraStackFrames) : base(testId) + { + Saml2Token = (Saml2SecurityToken)Saml2SecurityTokenHandler.CreateToken( + new SecurityTokenDescriptor() + { + Subject = Default.SamlClaimsIdentity, + Issuer = Default.Issuer, + IssuedAt = utcNow, + NotBefore = utcNow, + Expires = utcNow + TimeSpan.FromHours(1), + }); + + ValidationParameters = new ValidationParameters + { + AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation, + AudienceValidator = SkipValidationDelegates.SkipAudienceValidation, + IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation, + IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation, + LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation, + SignatureValidator = SkipValidationDelegates.SkipSignatureValidation, + TokenReplayValidator = tokenReplayValidator, + TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation + }; + + ExtraStackFrames = extraStackFrames; + } + + public Saml2SecurityToken Saml2Token { get; } + + public Saml2SecurityTokenHandler Saml2SecurityTokenHandler { get; } = new Saml2SecurityTokenHandler(); + + public bool IsValid { get; set; } + + internal ValidationParameters? ValidationParameters { get; set; } + + internal TokenReplayValidationError? TokenReplayValidationError { get; set; } + + internal int ExtraStackFrames { get; } + } + } +} +#nullable restore diff --git a/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandler.Extensibility.TokenReplay.cs b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandler.Extensibility.TokenReplay.cs new file mode 100644 index 0000000000..c603ebcacd --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Saml.Tests/SamlSecurityTokenHandler.Extensibility.TokenReplay.cs @@ -0,0 +1,279 @@ +// 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.Logging; +using Microsoft.IdentityModel.TestUtils; + +using Xunit; + +#nullable enable +namespace Microsoft.IdentityModel.Tokens.Saml.Extensibility.Tests +{ + public partial class SamlSecurityTokenHandlerValidateTokenAsyncTests + { + [Theory, MemberData(nameof(TokenReplay_ExtensibilityTestCases), DisableDiscoveryEnumeration = true)] + public async Task ValidateTokenAsync_TokenReplayValidator_Extensibility(TokenReplayExtensibilityTheoryData theoryData) + { + var context = TestUtilities.WriteHeader($"{this}.{nameof(ValidateTokenAsync_TokenReplayValidator_Extensibility)}", theoryData); + context.IgnoreType = false; + for (int i = 0; i < theoryData.ExtraStackFrames; i++) + theoryData.TokenReplayValidationError!.AddStackFrame(new StackFrame(false)); + + try + { + ValidationResult validationResult = await theoryData.SamlSecurityTokenHandler.ValidateTokenAsync( + theoryData.SamlToken!, + theoryData.ValidationParameters!, + theoryData.CallContext, + CancellationToken.None); + + if (validationResult.IsValid) + { + context.Diffs.Add("validationResult.IsValid is true, expected false"); + } + else + { + ValidationError validationError = validationResult.UnwrapError(); + IdentityComparer.AreValidationErrorsEqual(validationError, theoryData.TokenReplayValidationError, context); + theoryData.ExpectedException.ProcessException(validationError.GetException(), context); + } + } + catch (Exception ex) + { + theoryData.ExpectedException.ProcessException(ex, context); + } + + TestUtilities.AssertFailIfErrors(context); + } + + public static TheoryData TokenReplay_ExtensibilityTestCases + { + get + { + var theoryData = new TheoryData(); + CallContext callContext = new CallContext(); + var utcNow = DateTime.UtcNow; + var expirationTime = utcNow + TimeSpan.FromHours(1); + + #region return CustomTokenReplayValidationError + // Test cases where delegate is overridden and return a CustomTokenReplayValidationError + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 160), + expirationTime) + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 175), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorUnknownExceptionDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate, + extraStackFrames: 1) + { + // CustomTokenReplayValidationError does not handle the exception type 'NotSupportedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(NotSupportedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate))), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorUnknownExceptionDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(NotSupportedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 205), + expirationTime), + }); + + // CustomTokenReplayValidationError : TokenReplayValidationError, ExceptionType: NotSupportedException : SystemException, ValidationFailureType: CustomAudienceValidationFailureType + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate)), + TokenReplayValidationError = new CustomTokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.CustomTokenReplayValidatorCustomExceptionCustomFailureTypeDelegate), null), + CustomTokenReplayValidationError.CustomTokenReplayValidationFailureType, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 190), + expirationTime), + }); + #endregion + + #region return TokenReplayValidationError + // Test cases where delegate is overridden and return an TokenReplayValidationError + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidationDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate, + extraStackFrames: 1) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidationDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 235), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenReplayDetectedException : SecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate, + extraStackFrames: 1) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenReplayDetectedException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenReplayDetectedException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomTokenReplayDetectedExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenReplayDetectedException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 259), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: CustomSecurityTokenException : SystemException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorCustomExceptionTypeDelegate", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate, + extraStackFrames: 1) + { + // TokenReplayValidationError does not handle the exception type 'CustomSecurityTokenException' + ExpectedException = ExpectedException.SecurityTokenException( + LogHelper.FormatInvariant( + Tokens.LogMessages.IDX10002, // "IDX10002: Unknown exception type returned. Type: '{0}'. Message: '{1}'."; + typeof(CustomSecurityTokenException), + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate))), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorCustomExceptionTypeDelegate), null), + ValidationFailureType.TokenReplayValidationFailed, + typeof(CustomSecurityTokenException), + new StackFrame("CustomTokenReplayValidationDelegates.cs", 274), + expirationTime) + }); + + // TokenReplayValidationError : ValidationError, ExceptionType: SecurityTokenReplayDetectedException, inner: CustomSecurityTokenReplayDetectedException + theoryData.Add(new TokenReplayExtensibilityTheoryData( + "TokenReplayValidatorThrows", + utcNow, + CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows, + extraStackFrames: 0) + { + ExpectedException = new ExpectedException( + typeof(SecurityTokenReplayDetectedException), + string.Format(Tokens.LogMessages.IDX10276), + typeof(CustomSecurityTokenReplayDetectedException)), + TokenReplayValidationError = new TokenReplayValidationError( + new MessageDetail( + string.Format(Tokens.LogMessages.IDX10276), null), + ValidationFailureType.TokenReplayValidatorThrew, + typeof(SecurityTokenReplayDetectedException), + new StackFrame("SamlSecurityTokenHandler.ValidateToken.Internal.cs", 250), + expirationTime, + new SecurityTokenReplayDetectedException(nameof(CustomTokenReplayValidationDelegates.TokenReplayValidatorThrows)) + ) + }); + #endregion + + return theoryData; + } + } + + public class TokenReplayExtensibilityTheoryData : TheoryDataBase + { + internal TokenReplayExtensibilityTheoryData(string testId, DateTime utcNow, TokenReplayValidationDelegate tokenReplayValidator, int extraStackFrames) : base(testId) + { + SamlToken = (SamlSecurityToken)SamlSecurityTokenHandler.CreateToken( + new SecurityTokenDescriptor() + { + Subject = Default.SamlClaimsIdentity, + Issuer = Default.Issuer, + IssuedAt = utcNow, + NotBefore = utcNow, + Expires = utcNow + TimeSpan.FromHours(1), + }); + + ValidationParameters = new ValidationParameters + { + AlgorithmValidator = SkipValidationDelegates.SkipAlgorithmValidation, + AudienceValidator = SkipValidationDelegates.SkipAudienceValidation, + IssuerValidatorAsync = SkipValidationDelegates.SkipIssuerValidation, + IssuerSigningKeyValidator = SkipValidationDelegates.SkipIssuerSigningKeyValidation, + LifetimeValidator = SkipValidationDelegates.SkipLifetimeValidation, + SignatureValidator = SkipValidationDelegates.SkipSignatureValidation, + TokenReplayValidator = tokenReplayValidator, + TokenTypeValidator = SkipValidationDelegates.SkipTokenTypeValidation + }; + + ExtraStackFrames = extraStackFrames; + } + + public SamlSecurityToken SamlToken { get; } + + public SamlSecurityTokenHandler SamlSecurityTokenHandler { get; } = new SamlSecurityTokenHandler(); + + public bool IsValid { get; set; } + + internal ValidationParameters? ValidationParameters { get; set; } + + internal TokenReplayValidationError? TokenReplayValidationError { get; set; } + + internal int ExtraStackFrames { get; } + } + } +} +#nullable restore