Skip to content

Commit a7ef799

Browse files
authored
Read Token: Remove Exceptions (#2702)
* Added new version of ReadToken that receives a CallContext and returns a result wrapping exceptions and logs.
1 parent 245c831 commit a7ef799

File tree

5 files changed

+345
-0
lines changed

5 files changed

+345
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using Microsoft.IdentityModel.Logging;
6+
using Microsoft.IdentityModel.Tokens;
7+
using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
8+
9+
namespace Microsoft.IdentityModel.JsonWebTokens
10+
{
11+
/// <remarks>This partial class contains methods and logic related to the validation of tokens.</remarks>
12+
public partial class JsonWebTokenHandler : TokenHandler
13+
{
14+
#nullable enable
15+
/// <summary>
16+
/// Converts a string into an instance of <see cref="JsonWebToken"/>, returned inside of a <see cref="TokenReadingResult"/>.
17+
/// </summary>
18+
/// <param name="token">A JSON Web Token (JWT) in JWS or JWE Compact Serialization format.</param>
19+
/// <param name="callContext"></param>
20+
/// <returns>A <see cref="TokenReadingResult"/> with the <see cref="JsonWebToken"/> if valid, or an Exception.</returns>
21+
/// <exception cref="ArgumentNullException">returned if <paramref name="token"/> is null or empty.</exception>
22+
/// <exception cref="SecurityTokenMalformedException">returned if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid <see cref="JsonWebToken"/>.</exception>
23+
/// <exception cref="SecurityTokenMalformedException">returned if <paramref name="token"/> is not a valid JWT, <see cref="JsonWebToken"/>.</exception>
24+
internal static TokenReadingResult ReadToken(
25+
string token,
26+
#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
27+
CallContext? callContext)
28+
#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
29+
{
30+
if (String.IsNullOrEmpty(token))
31+
{
32+
return new TokenReadingResult(
33+
token,
34+
ValidationFailureType.NullArgument,
35+
new ExceptionDetail(
36+
new MessageDetail(
37+
TokenLogMessages.IDX10000,
38+
LogHelper.MarkAsNonPII(nameof(token))),
39+
typeof(ArgumentNullException),
40+
new System.Diagnostics.StackFrame()));
41+
}
42+
43+
try
44+
{
45+
JsonWebToken jsonWebToken = new JsonWebToken(token);
46+
return new TokenReadingResult(jsonWebToken, token);
47+
}
48+
#pragma warning disable CA1031 // Do not catch general exception types
49+
catch (Exception ex)
50+
#pragma warning restore CA1031 // Do not catch general exception types
51+
{
52+
return new TokenReadingResult(
53+
token,
54+
ValidationFailureType.TokenReadingFailed,
55+
new ExceptionDetail(
56+
new MessageDetail(LogMessages.IDX14107),
57+
ex.GetType(),
58+
new System.Diagnostics.StackFrame(),
59+
ex));
60+
}
61+
}
62+
}
63+
}
64+
#nullable restore
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
#nullable enable
7+
namespace Microsoft.IdentityModel.Tokens
8+
{
9+
/// <summary>
10+
/// Contains the result of reading a <see cref="SecurityToken"/>.
11+
/// The <see cref="TokenValidationResult"/> contains a collection of <see cref="ValidationResult"/> for each step in the token validation.
12+
/// </summary>
13+
internal class TokenReadingResult : ValidationResult
14+
{
15+
private Exception? _exception;
16+
private SecurityToken? _securityToken;
17+
18+
/// <summary>
19+
/// Creates an instance of <see cref="TokenReadingResult"/>.
20+
/// </summary>
21+
/// <paramref name="tokenInput"/> is the string from which the <see cref="SecurityToken"/> was created.
22+
/// <paramref name="securityToken"/> is the <see cref="SecurityToken"/> that was created.
23+
public TokenReadingResult(SecurityToken securityToken, string tokenInput)
24+
: base(ValidationFailureType.ValidationSucceeded)
25+
{
26+
IsValid = true;
27+
TokenInput = tokenInput;
28+
_securityToken = securityToken;
29+
}
30+
31+
/// <summary>
32+
/// Creates an instance of <see cref="TokenReadingResult"/>
33+
/// </summary>
34+
/// <paramref name="tokenInput"/> is the string that failed to create a <see cref="SecurityToken"/>.
35+
/// <paramref name="validationFailure"/> is the <see cref="ValidationFailureType"/> that occurred during reading.
36+
/// <paramref name="exceptionDetail"/> is the <see cref="ExceptionDetail"/> that occurred during reading.
37+
public TokenReadingResult(string? tokenInput, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
38+
: base(validationFailure, exceptionDetail)
39+
{
40+
TokenInput = tokenInput;
41+
IsValid = false;
42+
}
43+
44+
/// <summary>
45+
/// Gets the <see cref="SecurityToken"/> that was read.
46+
/// </summary>
47+
/// <exception cref="InvalidOperationException"/> if the <see cref="SecurityToken"/> is null.
48+
/// <remarks>It is expected that the caller would check <see cref="ValidationResult.IsValid"/> returns true before accessing this.</remarks>
49+
public SecurityToken SecurityToken()
50+
{
51+
if (_securityToken is null)
52+
throw new InvalidOperationException("Attempted to retrieve the SecurityToken from a failed TokenReading result.");
53+
54+
return _securityToken;
55+
}
56+
57+
/// <summary>
58+
/// Gets the <see cref="Exception"/> that occurred during reading.
59+
/// </summary>
60+
public override Exception? Exception
61+
{
62+
get
63+
{
64+
if (_exception != null || ExceptionDetail == null)
65+
return _exception;
66+
67+
HasValidOrExceptionWasRead = true;
68+
_exception = ExceptionDetail.GetException();
69+
70+
if (_exception is SecurityTokenException securityTokenException)
71+
{
72+
securityTokenException.Source = "Microsoft.IdentityModel.Tokens";
73+
securityTokenException.ExceptionDetail = ExceptionDetail;
74+
}
75+
76+
return _exception;
77+
}
78+
}
79+
80+
/// <summary>
81+
/// Gets the string from which the <see cref="SecurityToken"/> was read.
82+
/// </summary>
83+
public string? TokenInput { get; }
84+
}
85+
}
86+
#nullable restore

src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,12 @@ private class LifetimeValidationFailure : ValidationFailureType { internal Lifet
6969
public static readonly ValidationFailureType TokenReplayValidationFailed = new TokenReplayValidationFailure("TokenReplayValidationFailed");
7070
private class TokenReplayValidationFailure : ValidationFailureType { internal TokenReplayValidationFailure(string name) : base(name) { } }
7171

72+
/// <summary>
73+
/// Defines a type that represents that a token could not be read.
74+
/// </summary>
75+
public static readonly ValidationFailureType TokenReadingFailed = new TokenReadingFailure("TokenReadingFailed");
76+
private class TokenReadingFailure : ValidationFailureType { internal TokenReadingFailure(string name) : base(name) { } }
77+
7278
/// <summary>
7379
/// Defines a type that represents that no evaluation has taken place.
7480
/// </summary>
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IdentityModel.Tokens.Jwt.Tests;
6+
using Microsoft.IdentityModel.Logging;
7+
using Microsoft.IdentityModel.TestUtils;
8+
using Microsoft.IdentityModel.Tokens;
9+
using Xunit;
10+
using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
11+
12+
namespace Microsoft.IdentityModel.JsonWebTokens.Tests
13+
{
14+
public class JsonWebTokenHandlerReadTokenTests
15+
{
16+
[Theory, MemberData(nameof(JsonWebTokenHandlerReadTokenTestCases), DisableDiscoveryEnumeration = true)]
17+
public void ReadToken(TokenReadingTheoryData theoryData)
18+
{
19+
CompareContext context = TestUtilities.WriteHeader($"{this}.JsonWebTokenHandlerReadTokenTests", theoryData);
20+
TokenReadingResult tokenReadingResult = JsonWebTokenHandler.ReadToken(
21+
theoryData.Token,
22+
new CallContext());
23+
24+
if (tokenReadingResult.Exception != null)
25+
theoryData.ExpectedException.ProcessException(tokenReadingResult.Exception);
26+
else
27+
theoryData.ExpectedException.ProcessNoException();
28+
29+
IdentityComparer.AreTokenReadingResultsEqual(
30+
tokenReadingResult,
31+
theoryData.TokenReadingResult,
32+
context);
33+
34+
TestUtilities.AssertFailIfErrors(context);
35+
}
36+
37+
[Fact]
38+
public void ReadToken_ThrowsIfAccessingSecurityTokenOnFailedRead()
39+
{
40+
TokenReadingResult tokenReadingResult = JsonWebTokenHandler.ReadToken(
41+
null,
42+
new CallContext());
43+
44+
Assert.Throws<InvalidOperationException>(() => tokenReadingResult.SecurityToken());
45+
}
46+
47+
public static TheoryData<TokenReadingTheoryData> JsonWebTokenHandlerReadTokenTestCases
48+
{
49+
get
50+
{
51+
var validToken = EncodedJwts.LiveJwt;
52+
return new TheoryData<TokenReadingTheoryData>
53+
{
54+
new TokenReadingTheoryData
55+
{
56+
TestId = "Valid_Jwt",
57+
Token = validToken,
58+
TokenReadingResult = new TokenReadingResult(
59+
new JsonWebToken(validToken),
60+
validToken)
61+
},
62+
new TokenReadingTheoryData
63+
{
64+
TestId = "Invalid_NullToken",
65+
Token = null,
66+
ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"),
67+
TokenReadingResult = new TokenReadingResult(
68+
null,
69+
ValidationFailureType.NullArgument,
70+
new ExceptionDetail(
71+
new MessageDetail(
72+
TokenLogMessages.IDX10000,
73+
LogHelper.MarkAsNonPII("token")),
74+
typeof(ArgumentNullException),
75+
new System.Diagnostics.StackFrame()))
76+
},
77+
new TokenReadingTheoryData
78+
{
79+
TestId = "Invalid_EmptyToken",
80+
Token = string.Empty,
81+
ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"),
82+
TokenReadingResult = new TokenReadingResult(
83+
string.Empty,
84+
ValidationFailureType.NullArgument,
85+
new ExceptionDetail(
86+
new MessageDetail(
87+
TokenLogMessages.IDX10000,
88+
LogHelper.MarkAsNonPII("token")),
89+
typeof(ArgumentNullException),
90+
new System.Diagnostics.StackFrame()))
91+
},
92+
new TokenReadingTheoryData
93+
{
94+
TestId = "Invalid_MalformedToken",
95+
Token = "malformed-token",
96+
ExpectedException = ExpectedException.SecurityTokenMalformedTokenException(
97+
"IDX14107:",
98+
typeof(SecurityTokenMalformedException)),
99+
TokenReadingResult = new TokenReadingResult(
100+
"malformed-token",
101+
ValidationFailureType.TokenReadingFailed,
102+
new ExceptionDetail(
103+
new MessageDetail(
104+
LogMessages.IDX14107,
105+
LogHelper.MarkAsNonPII("token")),
106+
typeof(SecurityTokenMalformedException),
107+
new System.Diagnostics.StackFrame()))
108+
}
109+
};
110+
}
111+
}
112+
}
113+
114+
public class TokenReadingTheoryData : TheoryDataBase
115+
{
116+
public string Token { get; set; }
117+
public object TokenReadingResult { get; set; }
118+
}
119+
}

test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,76 @@ internal static bool AreTokenTypeValidationResultsEqual(
10071007
return context.Merge(localContext);
10081008
}
10091009

1010+
public static bool AreTokenReadingResultsEqual(object object1, object object2, CompareContext context)
1011+
{
1012+
var localContext = new CompareContext(context);
1013+
if (!ContinueCheckingEquality(object1, object2, context))
1014+
return context.Merge(localContext);
1015+
1016+
return AreTokenReadingResultsEqual(
1017+
object1 as TokenReadingResult,
1018+
object2 as TokenReadingResult,
1019+
"TokenReadingResult1",
1020+
"TokenReadingResult2",
1021+
null,
1022+
context);
1023+
}
1024+
1025+
internal static bool AreTokenReadingResultsEqual(
1026+
TokenReadingResult tokenReadingResult1,
1027+
TokenReadingResult tokenReadingResult2,
1028+
string name1,
1029+
string name2,
1030+
string stackPrefix,
1031+
CompareContext context)
1032+
{
1033+
var localContext = new CompareContext(context);
1034+
if (!ContinueCheckingEquality(tokenReadingResult1, tokenReadingResult2, localContext))
1035+
return context.Merge(localContext);
1036+
1037+
if (tokenReadingResult1.IsValid != tokenReadingResult2.IsValid)
1038+
localContext.Diffs.Add($"TokenReadingResult1.IsValid: {tokenReadingResult1.IsValid} != TokenReadingResult2.IsValid: {tokenReadingResult2.IsValid}");
1039+
1040+
if (tokenReadingResult1.TokenInput != tokenReadingResult2.TokenInput)
1041+
localContext.Diffs.Add($"TokenReadingResult1.TokenInput: '{tokenReadingResult1.TokenInput}' != TokenReadingResult2.TokenInput: '{tokenReadingResult2.TokenInput}'");
1042+
1043+
// Only compare the security token if both are valid.
1044+
if (tokenReadingResult1.IsValid && (tokenReadingResult1.SecurityToken().ToString() != tokenReadingResult2.SecurityToken().ToString()))
1045+
localContext.Diffs.Add($"TokenReadingResult1.SecurityToken: '{tokenReadingResult1.SecurityToken()}' != TokenReadingResult2.SecurityToken: '{tokenReadingResult2.SecurityToken()}'");
1046+
1047+
if (tokenReadingResult1.ValidationFailureType != tokenReadingResult2.ValidationFailureType)
1048+
localContext.Diffs.Add($"TokenReadingResult1.ValidationFailureType: {tokenReadingResult1.ValidationFailureType} != TokenReadingResult2.ValidationFailureType: {tokenReadingResult2.ValidationFailureType}");
1049+
1050+
// true => both are not null.
1051+
if (ContinueCheckingEquality(tokenReadingResult1.Exception, tokenReadingResult2.Exception, localContext))
1052+
{
1053+
AreStringsEqual(
1054+
tokenReadingResult1.Exception.Message,
1055+
tokenReadingResult2.Exception.Message,
1056+
$"({name1}).Exception.Message",
1057+
$"({name2}).Exception.Message",
1058+
localContext);
1059+
1060+
AreStringsEqual(
1061+
tokenReadingResult1.Exception.Source,
1062+
tokenReadingResult2.Exception.Source,
1063+
$"({name1}).Exception.Source",
1064+
$"({name2}).Exception.Source",
1065+
localContext);
1066+
1067+
if (!string.IsNullOrEmpty(stackPrefix))
1068+
AreStringPrefixesEqual(
1069+
tokenReadingResult1.Exception.StackTrace.Trim(),
1070+
tokenReadingResult2.Exception.StackTrace.Trim(),
1071+
$"({name1}).Exception.StackTrace",
1072+
$"({name2}).Exception.StackTrace",
1073+
stackPrefix.Trim(),
1074+
localContext);
1075+
}
1076+
1077+
return context.Merge(localContext);
1078+
}
1079+
10101080
public static bool AreJArraysEqual(object object1, object object2, CompareContext context)
10111081
{
10121082
var localContext = new CompareContext(context);

0 commit comments

Comments
 (0)