diff --git a/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ReadToken.cs b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ReadToken.cs
new file mode 100644
index 0000000000..66c9b194f9
--- /dev/null
+++ b/src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ReadToken.cs
@@ -0,0 +1,64 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using Microsoft.IdentityModel.Logging;
+using Microsoft.IdentityModel.Tokens;
+using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
+
+namespace Microsoft.IdentityModel.JsonWebTokens
+{
+ /// This partial class contains methods and logic related to the validation of tokens.
+ public partial class JsonWebTokenHandler : TokenHandler
+ {
+#nullable enable
+ ///
+ /// Converts a string into an instance of , returned inside of a .
+ ///
+ /// A JSON Web Token (JWT) in JWS or JWE Compact Serialization format.
+ ///
+ /// A with the if valid, or an Exception.
+ /// returned if is null or empty.
+ /// returned if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid .
+ /// returned if is not a valid JWT, .
+ internal static TokenReadingResult ReadToken(
+ string token,
+#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
+ CallContext? callContext)
+#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
+ {
+ if (String.IsNullOrEmpty(token))
+ {
+ return new TokenReadingResult(
+ token,
+ ValidationFailureType.NullArgument,
+ new ExceptionDetail(
+ new MessageDetail(
+ TokenLogMessages.IDX10000,
+ LogHelper.MarkAsNonPII(nameof(token))),
+ typeof(ArgumentNullException),
+ new System.Diagnostics.StackFrame()));
+ }
+
+ try
+ {
+ JsonWebToken jsonWebToken = new JsonWebToken(token);
+ return new TokenReadingResult(jsonWebToken, token);
+ }
+#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 TokenReadingResult(
+ token,
+ ValidationFailureType.TokenReadingFailed,
+ new ExceptionDetail(
+ new MessageDetail(LogMessages.IDX14107),
+ ex.GetType(),
+ new System.Diagnostics.StackFrame(),
+ ex));
+ }
+ }
+ }
+}
+#nullable restore
diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/TokenReadingResult.cs b/src/Microsoft.IdentityModel.Tokens/Validation/TokenReadingResult.cs
new file mode 100644
index 0000000000..38002bd071
--- /dev/null
+++ b/src/Microsoft.IdentityModel.Tokens/Validation/TokenReadingResult.cs
@@ -0,0 +1,86 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+
+#nullable enable
+namespace Microsoft.IdentityModel.Tokens
+{
+ ///
+ /// Contains the result of reading a .
+ /// The contains a collection of for each step in the token validation.
+ ///
+ internal class TokenReadingResult : ValidationResult
+ {
+ private Exception? _exception;
+ private SecurityToken? _securityToken;
+
+ ///
+ /// Creates an instance of .
+ ///
+ /// is the string from which the was created.
+ /// is the that was created.
+ public TokenReadingResult(SecurityToken securityToken, string tokenInput)
+ : base(ValidationFailureType.ValidationSucceeded)
+ {
+ IsValid = true;
+ TokenInput = tokenInput;
+ _securityToken = securityToken;
+ }
+
+ ///
+ /// Creates an instance of
+ ///
+ /// is the string that failed to create a .
+ /// is the that occurred during reading.
+ /// is the that occurred during reading.
+ public TokenReadingResult(string? tokenInput, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
+ : base(validationFailure, exceptionDetail)
+ {
+ TokenInput = tokenInput;
+ IsValid = false;
+ }
+
+ ///
+ /// Gets the that was read.
+ ///
+ /// if the is null.
+ /// It is expected that the caller would check returns true before accessing this.
+ public SecurityToken SecurityToken()
+ {
+ if (_securityToken is null)
+ throw new InvalidOperationException("Attempted to retrieve the SecurityToken from a failed TokenReading result.");
+
+ return _securityToken;
+ }
+
+ ///
+ /// Gets the that occurred during reading.
+ ///
+ public override Exception? Exception
+ {
+ get
+ {
+ if (_exception != null || ExceptionDetail == null)
+ return _exception;
+
+ HasValidOrExceptionWasRead = true;
+ _exception = ExceptionDetail.GetException();
+
+ if (_exception is SecurityTokenException securityTokenException)
+ {
+ securityTokenException.Source = "Microsoft.IdentityModel.Tokens";
+ securityTokenException.ExceptionDetail = ExceptionDetail;
+ }
+
+ return _exception;
+ }
+ }
+
+ ///
+ /// Gets the string from which the was read.
+ ///
+ public string? TokenInput { get; }
+ }
+}
+#nullable restore
diff --git a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs
index 52af888ad2..f185d73eac 100644
--- a/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs
+++ b/src/Microsoft.IdentityModel.Tokens/Validation/ValidationFailureType.cs
@@ -69,6 +69,12 @@ private class LifetimeValidationFailure : ValidationFailureType { internal Lifet
public static readonly ValidationFailureType TokenReplayValidationFailed = new TokenReplayValidationFailure("TokenReplayValidationFailed");
private class TokenReplayValidationFailure : ValidationFailureType { internal TokenReplayValidationFailure(string name) : base(name) { } }
+ ///
+ /// Defines a type that represents that a token could not be read.
+ ///
+ public static readonly ValidationFailureType TokenReadingFailed = new TokenReadingFailure("TokenReadingFailed");
+ private class TokenReadingFailure : ValidationFailureType { internal TokenReadingFailure(string name) : base(name) { } }
+
///
/// Defines a type that represents that no evaluation has taken place.
///
diff --git a/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ReadTokenTests.cs b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ReadTokenTests.cs
new file mode 100644
index 0000000000..3e21280c17
--- /dev/null
+++ b/test/Microsoft.IdentityModel.JsonWebTokens.Tests/JsonWebTokenHandler.ReadTokenTests.cs
@@ -0,0 +1,119 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.IdentityModel.Tokens.Jwt.Tests;
+using Microsoft.IdentityModel.Logging;
+using Microsoft.IdentityModel.TestUtils;
+using Microsoft.IdentityModel.Tokens;
+using Xunit;
+using TokenLogMessages = Microsoft.IdentityModel.Tokens.LogMessages;
+
+namespace Microsoft.IdentityModel.JsonWebTokens.Tests
+{
+ public class JsonWebTokenHandlerReadTokenTests
+ {
+ [Theory, MemberData(nameof(JsonWebTokenHandlerReadTokenTestCases), DisableDiscoveryEnumeration = true)]
+ public void ReadToken(TokenReadingTheoryData theoryData)
+ {
+ CompareContext context = TestUtilities.WriteHeader($"{this}.JsonWebTokenHandlerReadTokenTests", theoryData);
+ TokenReadingResult tokenReadingResult = JsonWebTokenHandler.ReadToken(
+ theoryData.Token,
+ new CallContext());
+
+ if (tokenReadingResult.Exception != null)
+ theoryData.ExpectedException.ProcessException(tokenReadingResult.Exception);
+ else
+ theoryData.ExpectedException.ProcessNoException();
+
+ IdentityComparer.AreTokenReadingResultsEqual(
+ tokenReadingResult,
+ theoryData.TokenReadingResult,
+ context);
+
+ TestUtilities.AssertFailIfErrors(context);
+ }
+
+ [Fact]
+ public void ReadToken_ThrowsIfAccessingSecurityTokenOnFailedRead()
+ {
+ TokenReadingResult tokenReadingResult = JsonWebTokenHandler.ReadToken(
+ null,
+ new CallContext());
+
+ Assert.Throws(() => tokenReadingResult.SecurityToken());
+ }
+
+ public static TheoryData JsonWebTokenHandlerReadTokenTestCases
+ {
+ get
+ {
+ var validToken = EncodedJwts.LiveJwt;
+ return new TheoryData
+ {
+ new TokenReadingTheoryData
+ {
+ TestId = "Valid_Jwt",
+ Token = validToken,
+ TokenReadingResult = new TokenReadingResult(
+ new JsonWebToken(validToken),
+ validToken)
+ },
+ new TokenReadingTheoryData
+ {
+ TestId = "Invalid_NullToken",
+ Token = null,
+ ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"),
+ TokenReadingResult = new TokenReadingResult(
+ null,
+ ValidationFailureType.NullArgument,
+ new ExceptionDetail(
+ new MessageDetail(
+ TokenLogMessages.IDX10000,
+ LogHelper.MarkAsNonPII("token")),
+ typeof(ArgumentNullException),
+ new System.Diagnostics.StackFrame()))
+ },
+ new TokenReadingTheoryData
+ {
+ TestId = "Invalid_EmptyToken",
+ Token = string.Empty,
+ ExpectedException = ExpectedException.ArgumentNullException("IDX10000:"),
+ TokenReadingResult = new TokenReadingResult(
+ string.Empty,
+ ValidationFailureType.NullArgument,
+ new ExceptionDetail(
+ new MessageDetail(
+ TokenLogMessages.IDX10000,
+ LogHelper.MarkAsNonPII("token")),
+ typeof(ArgumentNullException),
+ new System.Diagnostics.StackFrame()))
+ },
+ new TokenReadingTheoryData
+ {
+ TestId = "Invalid_MalformedToken",
+ Token = "malformed-token",
+ ExpectedException = ExpectedException.SecurityTokenMalformedTokenException(
+ "IDX14107:",
+ typeof(SecurityTokenMalformedException)),
+ TokenReadingResult = new TokenReadingResult(
+ "malformed-token",
+ ValidationFailureType.TokenReadingFailed,
+ new ExceptionDetail(
+ new MessageDetail(
+ LogMessages.IDX14107,
+ LogHelper.MarkAsNonPII("token")),
+ typeof(SecurityTokenMalformedException),
+ new System.Diagnostics.StackFrame()))
+ }
+ };
+ }
+ }
+ }
+
+ public class TokenReadingTheoryData : TheoryDataBase
+ {
+ public string Token { get; set; }
+ public object TokenReadingResult { get; set; }
+ }
+}
diff --git a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
index 2e061f2ce2..47b868d8a4 100644
--- a/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
+++ b/test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
@@ -1007,6 +1007,76 @@ internal static bool AreTokenTypeValidationResultsEqual(
return context.Merge(localContext);
}
+ public static bool AreTokenReadingResultsEqual(object object1, object object2, CompareContext context)
+ {
+ var localContext = new CompareContext(context);
+ if (!ContinueCheckingEquality(object1, object2, context))
+ return context.Merge(localContext);
+
+ return AreTokenReadingResultsEqual(
+ object1 as TokenReadingResult,
+ object2 as TokenReadingResult,
+ "TokenReadingResult1",
+ "TokenReadingResult2",
+ null,
+ context);
+ }
+
+ internal static bool AreTokenReadingResultsEqual(
+ TokenReadingResult tokenReadingResult1,
+ TokenReadingResult tokenReadingResult2,
+ string name1,
+ string name2,
+ string stackPrefix,
+ CompareContext context)
+ {
+ var localContext = new CompareContext(context);
+ if (!ContinueCheckingEquality(tokenReadingResult1, tokenReadingResult2, localContext))
+ return context.Merge(localContext);
+
+ if (tokenReadingResult1.IsValid != tokenReadingResult2.IsValid)
+ localContext.Diffs.Add($"TokenReadingResult1.IsValid: {tokenReadingResult1.IsValid} != TokenReadingResult2.IsValid: {tokenReadingResult2.IsValid}");
+
+ if (tokenReadingResult1.TokenInput != tokenReadingResult2.TokenInput)
+ localContext.Diffs.Add($"TokenReadingResult1.TokenInput: '{tokenReadingResult1.TokenInput}' != TokenReadingResult2.TokenInput: '{tokenReadingResult2.TokenInput}'");
+
+ // Only compare the security token if both are valid.
+ if (tokenReadingResult1.IsValid && (tokenReadingResult1.SecurityToken().ToString() != tokenReadingResult2.SecurityToken().ToString()))
+ localContext.Diffs.Add($"TokenReadingResult1.SecurityToken: '{tokenReadingResult1.SecurityToken()}' != TokenReadingResult2.SecurityToken: '{tokenReadingResult2.SecurityToken()}'");
+
+ if (tokenReadingResult1.ValidationFailureType != tokenReadingResult2.ValidationFailureType)
+ localContext.Diffs.Add($"TokenReadingResult1.ValidationFailureType: {tokenReadingResult1.ValidationFailureType} != TokenReadingResult2.ValidationFailureType: {tokenReadingResult2.ValidationFailureType}");
+
+ // true => both are not null.
+ if (ContinueCheckingEquality(tokenReadingResult1.Exception, tokenReadingResult2.Exception, localContext))
+ {
+ AreStringsEqual(
+ tokenReadingResult1.Exception.Message,
+ tokenReadingResult2.Exception.Message,
+ $"({name1}).Exception.Message",
+ $"({name2}).Exception.Message",
+ localContext);
+
+ AreStringsEqual(
+ tokenReadingResult1.Exception.Source,
+ tokenReadingResult2.Exception.Source,
+ $"({name1}).Exception.Source",
+ $"({name2}).Exception.Source",
+ localContext);
+
+ if (!string.IsNullOrEmpty(stackPrefix))
+ AreStringPrefixesEqual(
+ tokenReadingResult1.Exception.StackTrace.Trim(),
+ tokenReadingResult2.Exception.StackTrace.Trim(),
+ $"({name1}).Exception.StackTrace",
+ $"({name2}).Exception.StackTrace",
+ stackPrefix.Trim(),
+ localContext);
+ }
+
+ return context.Merge(localContext);
+ }
+
public static bool AreJArraysEqual(object object1, object object2, CompareContext context)
{
var localContext = new CompareContext(context);