Skip to content

Commit c964f16

Browse files
authored
New token validation model: Simplify stack frame caching (#2976)
* Added methods to ValidationError to simplify the caching of StackFrame objects by using a concurrent dictionary * Changed methods to be internal
1 parent 96b6b36 commit c964f16

File tree

3 files changed

+91
-1
lines changed

3 files changed

+91
-1
lines changed

src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Microsoft.IdentityModel.Tokens.TokenTypeValidationError.TokenTypeValidationError
1515
Microsoft.IdentityModel.Tokens.TokenTypeValidationError._invalidTokenType -> string
1616
Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.get -> System.TimeProvider
1717
Microsoft.IdentityModel.Tokens.TokenValidationParameters.TimeProvider.set -> void
18+
Microsoft.IdentityModel.Tokens.ValidationError.AddCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> Microsoft.IdentityModel.Tokens.ValidationError
1819
Microsoft.IdentityModel.Tokens.ValidationError.GetException(System.Type exceptionType, System.Exception innerException) -> System.Exception
1920
Microsoft.IdentityModel.Tokens.ValidationParameters.TokenTypeValidator.get -> Microsoft.IdentityModel.Tokens.TokenTypeValidationDelegate
2021
Microsoft.IdentityModel.Tokens.ValidationParameters.TokenTypeValidator.set -> void
@@ -29,6 +30,7 @@ static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidateAudienceFa
2930
static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParametersAudiencesCountZero -> System.Diagnostics.StackFrame
3031
static Microsoft.IdentityModel.Tokens.AudienceValidationError.ValidationParametersNull -> System.Diagnostics.StackFrame
3132
static Microsoft.IdentityModel.Tokens.Utility.SerializeAsSingleCommaDelimitedString(System.Collections.Generic.IList<string> strings) -> string
33+
static Microsoft.IdentityModel.Tokens.ValidationError.GetCurrentStackFrame(string filePath = "", int lineNumber = 0, int skipFrames = 1) -> System.Diagnostics.StackFrame
3234
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoTokenAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
3335
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.NoValidationParameterAudiencesProvided -> Microsoft.IdentityModel.Tokens.ValidationFailureType
3436
static readonly Microsoft.IdentityModel.Tokens.ValidationFailureType.SignatureAlgorithmValidationFailed -> Microsoft.IdentityModel.Tokens.ValidationFailureType

src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/ValidationError.cs

+44-1
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
67
using System.Diagnostics;
8+
using System.Runtime.CompilerServices;
79
using Microsoft.IdentityModel.Logging;
810

911
namespace Microsoft.IdentityModel.Tokens
@@ -73,7 +75,7 @@ internal Exception GetException(Type exceptionType, Exception innerException)
7375
if (innerException == null && InnerValidationError == null)
7476
{
7577
if (exceptionType == typeof(SecurityTokenArgumentNullException))
76-
return new SecurityTokenArgumentNullException(MessageDetail.Message);
78+
exception = new SecurityTokenArgumentNullException(MessageDetail.Message);
7779
else if (exceptionType == typeof(SecurityTokenInvalidAudienceException))
7880
exception = new SecurityTokenInvalidAudienceException(MessageDetail.Message);
7981
else if (exceptionType == typeof(SecurityTokenInvalidIssuerException))
@@ -187,6 +189,11 @@ internal Exception GetException(Type exceptionType, Exception innerException)
187189
}
188190
}
189191

192+
if (exception is SecurityTokenException securityTokenException)
193+
securityTokenException.SetValidationError(this);
194+
else if (exception is SecurityTokenArgumentNullException securityTokenArgumentNullException)
195+
securityTokenArgumentNullException.SetValidationError(this);
196+
190197
return exception;
191198
}
192199

@@ -236,5 +243,41 @@ public ValidationError AddStackFrame(StackFrame stackFrame)
236243
StackFrames.Add(stackFrame);
237244
return this;
238245
}
246+
247+
/// <summary>
248+
/// Adds the current stack frame to the list of stack frames and returns the updated object.
249+
/// 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.
250+
/// </summary>
251+
/// <param name="filePath">The path to the file from which this method is called. Captured automatically by default.</param>
252+
/// <param name="lineNumber">The line number from which this method is called. CAptured automatically by default.</param>
253+
/// <param name="skipFrames">The number of stack frames to skip when capturing. Used to avoid capturing this method and get the caller instead.</param>
254+
/// <returns>The updated object.</returns>
255+
internal ValidationError AddCurrentStackFrame([CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0, int skipFrames = 1)
256+
{
257+
// We add 1 to the skipped frames to skip the current method
258+
StackFrames.Add(GetCurrentStackFrame(filePath, lineNumber, skipFrames + 1));
259+
return this;
260+
}
261+
262+
/// <summary>
263+
/// Returns the stack frame corresponding to the file path and line number from which this method is called.
264+
/// 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.
265+
/// </summary>
266+
/// <param name="filePath">The path to the file from which this method is called. Captured automatically by default.</param>
267+
/// <param name="lineNumber">The line number from which this method is called. CAptured automatically by default.</param>
268+
/// <param name="skipFrames">The number of stack frames to skip when capturing. Used to avoid capturing this method and get the caller instead.</param>
269+
/// <returns>The captured stack frame.</returns>
270+
/// <remarks>If this is called from a helper method, consider adding an extra skip frame to avoid capturing the helper instead.</remarks>
271+
internal static StackFrame GetCurrentStackFrame(
272+
[CallerFilePath] string filePath = "", [CallerLineNumber] int lineNumber = 0, int skipFrames = 1)
273+
{
274+
// String is allocated, but it goes out of scope immediately after the call
275+
string key = filePath + lineNumber;
276+
StackFrame frame = CachedStackFrames.GetOrAdd(key, new StackFrame(skipFrames, true));
277+
return frame;
278+
}
279+
280+
// ConcurrentDictionary is thread-safe and only locks when adding a new item.
281+
private static ConcurrentDictionary<string, StackFrame> CachedStackFrames { get; } = new();
239282
}
240283
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Xunit;
5+
6+
namespace Microsoft.IdentityModel.Tokens.Tests
7+
{
8+
public class ValidationErrorTests
9+
{
10+
[Fact]
11+
public void ExceptionCreatedFromValidationError_ContainsTheRightStackTrace()
12+
{
13+
var validationError = new ValidationErrorReturningClass().firstMethod();
14+
Assert.NotNull(validationError);
15+
Assert.NotNull(validationError.StackFrames);
16+
Assert.Equal(3, validationError.StackFrames.Count);
17+
Assert.NotNull(validationError.GetException());
18+
Assert.NotNull(validationError.GetException().StackTrace);
19+
Assert.Equal("thirdMethod", validationError.StackFrames[0].GetMethod().Name);
20+
Assert.Equal("secondMethod", validationError.StackFrames[1].GetMethod().Name);
21+
Assert.Equal("firstMethod", validationError.StackFrames[2].GetMethod().Name);
22+
}
23+
class ValidationErrorReturningClass
24+
{
25+
public ValidationError firstMethod()
26+
{
27+
return secondMethod().AddCurrentStackFrame();
28+
}
29+
30+
public ValidationError secondMethod()
31+
{
32+
return thirdMethod().AddCurrentStackFrame();
33+
}
34+
35+
public ValidationError thirdMethod()
36+
{
37+
return new ValidationError(
38+
new MessageDetail("This is a test error"),
39+
ValidationFailureType.NullArgument,
40+
typeof(SecurityTokenArgumentNullException),
41+
ValidationError.GetCurrentStackFrame());
42+
}
43+
}
44+
}
45+
}

0 commit comments

Comments
 (0)