Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Read Token: Remove Exceptions #2702

Merged
merged 5 commits into from
Jul 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
{
/// <remarks>This partial class contains methods and logic related to the validation of tokens.</remarks>
public partial class JsonWebTokenHandler : TokenHandler
{
#nullable enable
/// <summary>
/// Converts a string into an instance of <see cref="JsonWebToken"/>, returned inside of a <see cref="TokenReadingResult"/>.
/// </summary>
/// <param name="token">A JSON Web Token (JWT) in JWS or JWE Compact Serialization format.</param>
/// <param name="callContext"></param>
/// <returns>A <see cref="TokenReadingResult"/> with the <see cref="JsonWebToken"/> if valid, or an Exception.</returns>
/// <exception cref="ArgumentNullException">returned if <paramref name="token"/> is null or empty.</exception>
/// <exception cref="SecurityTokenMalformedException">returned if the validationParameters.TokenReader delegate is not able to parse/read the token as a valid <see cref="JsonWebToken"/>.</exception>
/// <exception cref="SecurityTokenMalformedException">returned if <paramref name="token"/> is not a valid JWT, <see cref="JsonWebToken"/>.</exception>
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// Contains the result of reading a <see cref="SecurityToken"/>.
/// The <see cref="TokenValidationResult"/> contains a collection of <see cref="ValidationResult"/> for each step in the token validation.
/// </summary>
internal class TokenReadingResult : ValidationResult
{
private Exception? _exception;
private SecurityToken? _securityToken;

/// <summary>
/// Creates an instance of <see cref="TokenReadingResult"/>.
/// </summary>
/// <paramref name="tokenInput"/> is the string from which the <see cref="SecurityToken"/> was created.
/// <paramref name="securityToken"/> is the <see cref="SecurityToken"/> that was created.
public TokenReadingResult(SecurityToken securityToken, string tokenInput)
: base(ValidationFailureType.ValidationSucceeded)
{
IsValid = true;
TokenInput = tokenInput;
_securityToken = securityToken;
}

/// <summary>
/// Creates an instance of <see cref="TokenReadingResult"/>
/// </summary>
/// <paramref name="tokenInput"/> is the string that failed to create a <see cref="SecurityToken"/>.
/// <paramref name="validationFailure"/> is the <see cref="ValidationFailureType"/> that occurred during reading.
/// <paramref name="exceptionDetail"/> is the <see cref="ExceptionDetail"/> that occurred during reading.
public TokenReadingResult(string? tokenInput, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
: base(validationFailure, exceptionDetail)
{
TokenInput = tokenInput;
IsValid = false;
}

/// <summary>
/// Gets the <see cref="SecurityToken"/> that was read.
/// </summary>
/// <exception cref="InvalidOperationException"/> if the <see cref="SecurityToken"/> is null.
/// <remarks>It is expected that the caller would check <see cref="ValidationResult.IsValid"/> returns true before accessing this.</remarks>
public SecurityToken SecurityToken()
{
if (_securityToken is null)
throw new InvalidOperationException("Attempted to retrieve the SecurityToken from a failed TokenReading result.");

return _securityToken;
}

/// <summary>
/// Gets the <see cref="Exception"/> that occurred during reading.
/// </summary>
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;
}
}

/// <summary>
/// Gets the string from which the <see cref="SecurityToken"/> was read.
/// </summary>
public string? TokenInput { get; }
}
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,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) { } }

/// <summary>
/// Defines a type that represents that a token could not be read.
/// </summary>
public static readonly ValidationFailureType TokenReadingFailed = new TokenReadingFailure("TokenReadingFailed");
private class TokenReadingFailure : ValidationFailureType { internal TokenReadingFailure(string name) : base(name) { } }

/// <summary>
/// Defines a type that represents that no evaluation has taken place.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<InvalidOperationException>(() => tokenReadingResult.SecurityToken());
}

public static TheoryData<TokenReadingTheoryData> JsonWebTokenHandlerReadTokenTestCases
{
get
{
var validToken = EncodedJwts.LiveJwt;
return new TheoryData<TokenReadingTheoryData>
{
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; }
}
}
70 changes: 70 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,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);
Expand Down