diff --git a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt index e50d151726..1d35266764 100644 --- a/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt +++ b/src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt @@ -15,6 +15,7 @@ Microsoft.IdentityModel.Tokens.TokenTypeValidationError.TokenTypeValidationError Microsoft.IdentityModel.Tokens.TokenTypeValidationError._invalidTokenType -> string Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.get -> System.TimeProvider Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.set -> void +Microsoft.IdentityModel.Tokens.ValidationError.AddCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> Microsoft.IdentityModel.Tokens.ValidationError Microsoft.IdentityModel.Tokens.ValidationError.GetException(System.Type exceptionType, System.Exception innerException) -> System.Exception Microsoft.IdentityModel.Tokens.ValidationParameters.TokenTypeValidator.get -> Microsoft.IdentityModel.Tokens.TokenTypeValidationDelegate Microsoft.IdentityModel.Tokens.ValidationParameters.TokenTypeValidator.set -> void @@ -29,6 +30,7 @@ static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidateAudienceFa static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParametersAudiencesCountZero -> System.Diagnostics.StackFrame static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParametersNull -> System.Diagnostics.StackFrame 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 static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs index 8dec8466a0..37bb29c092 100644 --- a/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs +++ b/src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs @@ -2,8 +2,10 @@ // Licensed under the MIT License. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Runtime.CompilerServices; using Microsoft.IdentityModel.Logging; namespace Microsoft.IdentityModel.Tokens @@ -73,7 +75,7 @@ internal Exception GetException(Type exceptionType, Exception innerException) if (innerException == null && InnerValidationError == null) { if (exceptionType == typeof(SecurityTokenArgumentNullException)) - return new SecurityTokenArgumentNullException(MessageDetail.Message); + exception = new SecurityTokenArgumentNullException(MessageDetail.Message); else if (exceptionType == typeof(SecurityTokenInvalidAudienceException)) exception = new SecurityTokenInvalidAudienceException(MessageDetail.Message); else if (exceptionType == typeof(SecurityTokenInvalidIssuerException)) @@ -187,6 +189,11 @@ internal Exception GetException(Type exceptionType, Exception innerException) } } + if (exception is SecurityTokenException securityTokenException) + securityTokenException.SetValidationError(this); + else if (exception is SecurityTokenArgumentNullException securityTokenArgumentNullException) + securityTokenArgumentNullException.SetValidationError(this); + return exception; } @@ -236,5 +243,41 @@ public ValidationError AddStackFrame(StackFrame stackFrame) StackFrames.Add(stackFrame); return this; } + + /// + /// Adds the current stack frame to the list of stack frames and returns the updated object. + /// If there is no cache entry for the given file path and line number, a new stack frame is created and added to the cache. + /// + /// The path to the file from which this method is called. Captured automatically by default. + /// The line number from which this method is called. CAptured automatically by default. + /// The number of stack frames to skip when capturing. Used to avoid capturing this method and get the caller instead. + /// The updated object. + internal ValidationError AddCurrentStackFrame([CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0, int skipFrames = 1) + { + // We add 1 to the skipped frames to skip the current method + StackFrames.Add(GetCurrentStackFrame(filePath, lineNumber, skipFrames + 1)); + return this; + } + + /// + /// Returns the stack frame corresponding to the file path and line number from which this method is called. + /// If there is no cache entry for the given file path and line number, a new stack frame is created and added to the cache. + /// + /// The path to the file from which this method is called. Captured automatically by default. + /// The line number from which this method is called. CAptured automatically by default. + /// The number of stack frames to skip when capturing. Used to avoid capturing this method and get the caller instead. + /// The captured stack frame. + /// If this is called from a helper method, consider adding an extra skip frame to avoid capturing the helper instead. + internal static StackFrame GetCurrentStackFrame( + [CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0, int skipFrames = 1) + { + // String is allocated, but it goes out of scope immediately after the call + string key = filePath + lineNumber; + StackFrame frame = CachedStackFrames.GetOrAdd(key, new StackFrame(skipFrames, true)); + return frame; + } + + // ConcurrentDictionary is thread-safe and only locks when adding a new item. + private static ConcurrentDictionary CachedStackFrames { get; } = new(); } } diff --git a/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationErrorTests.cs b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationErrorTests.cs new file mode 100644 index 0000000000..c1151641f8 --- /dev/null +++ b/test/Microsoft.IdentityModel.Tokens.Tests/Validation/ValidationErrorTests.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; + +namespace Microsoft.IdentityModel.Tokens.Tests +{ + public class ValidationErrorTests + { + [Fact] + public void ExceptionCreatedFromValidationError_ContainsTheRightStackTrace() + { + var validationError = new ValidationErrorReturningClass().firstMethod(); + Assert.NotNull(validationError); + Assert.NotNull(validationError.StackFrames); + Assert.Equal(3, validationError.StackFrames.Count); + Assert.NotNull(validationError.GetException()); + Assert.NotNull(validationError.GetException().StackTrace); + Assert.Equal("thirdMethod", validationError.StackFrames[0].GetMethod().Name); + Assert.Equal("secondMethod", validationError.StackFrames[1].GetMethod().Name); + Assert.Equal("firstMethod", validationError.StackFrames[2].GetMethod().Name); + } + class ValidationErrorReturningClass + { + public ValidationError firstMethod() + { + return secondMethod().AddCurrentStackFrame(); + } + + public ValidationError secondMethod() + { + return thirdMethod().AddCurrentStackFrame(); + } + + public ValidationError thirdMethod() + { + return new ValidationError( + new MessageDetail("This is a test error"), + ValidationFailureType.NullArgument, + typeof(SecurityTokenArgumentNullException), + ValidationError.GetCurrentStackFrame()); + } + } + } +}