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

Token Type validation: Remove exceptions #2688

Merged
merged 3 commits into from
Jul 9, 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
1 change: 1 addition & 0 deletions src/Microsoft.IdentityModel.Tokens/LogMessages.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ internal static class LogMessages
public const string IDX10256 = "IDX10256: Unable to validate the token type. TokenValidationParameters.ValidTypes is set, but the 'typ' header claim is null or empty.";
public const string IDX10257 = "IDX10257: Token type validation failed. Type: '{0}'. Did not match: validationParameters.TokenTypes: '{1}'.";
public const string IDX10258 = "IDX10258: Token type validated. Type: '{0}'.";
public const string IDX10259 = "IDX10259: Unable to validate the token type, delegate threw an exception.";
// public const string IDX10260 = "IDX10260:";
public const string IDX10261 = "IDX10261: Unable to retrieve configuration from authority: '{0}'. \nProceeding with token validation in case the relevant properties have been set manually on the TokenValidationParameters. Exception caught: \n {1}. See https://aka.ms/validate-using-configuration-manager for additional information.";
public const string IDX10262 = "IDX10262: One of the issuers in TokenValidationParameters.ValidIssuers was null or an empty string. See https://aka.ms/wilson/tokenvalidation for details.";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// 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 validating the TokenType of 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 TokenTypeValidationResult : ValidationResult
{
private Exception? _exception;
private const string TokenSource = "Microsoft.IdentityModel.Tokens";

/// <summary>
/// Creates an instance of <see cref="TokenTypeValidationResult"/>.
/// </summary>
/// <paramref name="type"/> is the type against which the token was validated.
public TokenTypeValidationResult(string? type)
: base(ValidationFailureType.ValidationSucceeded)
{
Type = type;
IsValid = true;
}

/// <summary>
/// Creates an instance of <see cref="TokenTypeValidationResult"/>
/// </summary>
/// <paramref name="type"/> is the type against which the token was validated.
/// <paramref name="validationFailure"/> is the <see cref="ValidationFailureType"/> that occurred during validation.
/// <paramref name="exceptionDetail"/> is the <see cref="ExceptionDetail"/> that occurred during validation.
public TokenTypeValidationResult(string? type, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
: base(validationFailure, exceptionDetail)
{
Type = type;
IsValid = false;
}

/// <summary>
/// Gets the <see cref="Exception"/> that occurred during validation.
/// </summary>
public override Exception? Exception
{
get
{
if (_exception != null || ExceptionDetail == null)
return _exception;

HasValidOrExceptionWasRead = true;
_exception = ExceptionDetail.GetException();
if (_exception is SecurityTokenInvalidTypeException securityTokenInvalidTypeException)
{
securityTokenInvalidTypeException.InvalidType = Type;
securityTokenInvalidTypeException.Source = TokenSource;
}

return _exception;
}
}

/// <summary>
/// Gets the security token type.
/// </summary>
public string? Type { get; }

}
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ private class IssuerValidationFailure : ValidationFailureType { internal IssuerV
public static readonly ValidationFailureType AudienceValidationFailed = new AudienceValidationFailure("AudienceValidationFailed");
private class AudienceValidationFailure : ValidationFailureType { internal AudienceValidationFailure(string name) : base(name) { } }

/// <summary>
/// Defines a type that represents that token type validation failed.
/// </summary>
public static readonly ValidationFailureType TokenTypeValidationFailed = new TokenTypeValidationFailure("TokenTypeValidationFailed");
private class TokenTypeValidationFailure : ValidationFailureType { internal TokenTypeValidationFailure(string name) : base(name) { } }

/// <summary>
/// Defines a type that represents that signing key validation failed.
/// </summary>
Expand All @@ -48,7 +54,7 @@ private class SigningKeyValidationFailure : ValidationFailureType { internal Sig
/// <summary>
/// Defines a type that represents that lifetime validation failed.
/// </summary>
public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailure");
public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailed");
private class LifetimeValidationFailure : ValidationFailureType { internal LifetimeValidationFailure(string name) : base(name) { } }

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// Licensed under the MIT License.

using System;
using System.Diagnostics;
using System.Linq;
using Microsoft.IdentityModel.Abstractions;
using Microsoft.IdentityModel.Logging;

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
public static partial class Validators
Expand Down Expand Up @@ -57,5 +59,121 @@ public static string ValidateTokenType(string type, SecurityToken securityToken,

return type;
}

/// <summary>
/// Validates the type of the token.
/// </summary>
/// <param name="type">The token type or <c>null</c> if it couldn't be resolved (e.g from the 'typ' header for a JWT).</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> that is being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="callContext"></param>
/// <exception cref="ArgumentNullException">If <paramref name="validationParameters"/> is null.</exception>
/// <exception cref="ArgumentNullException">If <paramref name="securityToken"/> is null.</exception>
/// <exception cref="SecurityTokenInvalidTypeException">If <paramref name="type"/> is null or whitespace and <see cref="TokenValidationParameters.ValidTypes"/> is not null.</exception>
/// <exception cref="SecurityTokenInvalidTypeException">If <paramref name="type"/> failed to match <see cref="TokenValidationParameters.ValidTypes"/>.</exception>
/// <remarks>An EXACT match is required. <see cref="StringComparison.Ordinal"/> (case sensitive) is used for comparing <paramref name="type"/> against <see cref="TokenValidationParameters.ValidTypes"/>.</remarks>
#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
internal static TokenTypeValidationResult ValidateTokenType(string? type, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext)
#pragma warning restore CA1801 // TODO: remove pragma disable once callContext is used for logging
{
if (securityToken == null)
{
return new TokenTypeValidationResult(
type,
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10000,
LogHelper.MarkAsNonPII(nameof(securityToken))),
typeof(ArgumentNullException),
new StackFrame(true)));
}

if (validationParameters == null)
{
return new TokenTypeValidationResult(
type,
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10000,
LogHelper.MarkAsNonPII(nameof(validationParameters))),
typeof(ArgumentNullException),
new StackFrame(true)));
}

if (validationParameters.TypeValidator == null && (validationParameters.ValidTypes == null || !validationParameters.ValidTypes.Any()))
{
LogHelper.LogVerbose(LogMessages.IDX10255);
return new TokenTypeValidationResult(type);
}

if (validationParameters.TypeValidator != null)
{
return ValidateTokenTypeUsingDelegate(type, securityToken, validationParameters);
}

// Note: don't return an invalid TokenTypeValidationResult for a null or empty token type when a user-defined delegate is set
// to allow it to extract the actual token type from a different location (e.g from the claims).
if (string.IsNullOrEmpty(type))
{
return new TokenTypeValidationResult(
type,
ValidationFailureType.TokenTypeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10256,
LogHelper.MarkAsNonPII(nameof(type))),
typeof(SecurityTokenInvalidTypeException),
new StackFrame(true)));
}

if (!validationParameters.ValidTypes.Contains(type, StringComparer.Ordinal))
{
return new TokenTypeValidationResult(
type,
ValidationFailureType.TokenTypeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10257,
LogHelper.MarkAsNonPII(nameof(type)),
LogHelper.MarkAsNonPII(Utility.SerializeAsSingleCommaDelimitedString(validationParameters.ValidTypes))),
typeof(SecurityTokenInvalidTypeException),
new StackFrame(true)));
}

if (LogHelper.IsEnabled(EventLogLevel.Informational))
{
LogHelper.LogInformation(LogMessages.IDX10258, LogHelper.MarkAsNonPII(type));
}

return new TokenTypeValidationResult(type);
}

private static TokenTypeValidationResult ValidateTokenTypeUsingDelegate(string? type, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
try
{
var validatedType = validationParameters.TypeValidator(type, securityToken, validationParameters);
return new TokenTypeValidationResult(validatedType);
}
#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 TokenTypeValidationResult(
type,
ValidationFailureType.TokenTypeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10259,
LogHelper.MarkAsNonPII(nameof(validationParameters.TypeValidator)),
LogHelper.MarkAsNonPII(ex.Message)),
ex.GetType(),
new StackFrame(true),
ex));
}
}
}
}
#nullable restore
5 changes: 5 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/ExpectedException.cs
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,11 @@ public static ExpectedException SecurityTokenInvalidSignatureException(string su
return new ExpectedException(typeof(SecurityTokenInvalidSignatureException), substringExpected, innerTypeExpected);
}

public static ExpectedException SecurityTokenInvalidTypeException(string substringExpected = null, Type innerTypeExpected = null)
{
return new ExpectedException(typeof(SecurityTokenInvalidTypeException), substringExpected, innerTypeExpected);
}

public static ExpectedException SecurityTokenNoExpirationException(string substringExpected = null, Type innerTypeExpected = null)
{
return new ExpectedException(typeof(SecurityTokenNoExpirationException), substringExpected, innerTypeExpected);
Expand Down
66 changes: 66 additions & 0 deletions test/Microsoft.IdentityModel.TestUtils/IdentityComparer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,72 @@ internal static bool AreTokenReplayValidationResultsEqual(
return context.Merge(localContext);
}

public static bool AreTokenTypeValidationResultsEqual(object object1, object object2, CompareContext context)
{
var localContext = new CompareContext(context);
if (!ContinueCheckingEquality(object1, object2, context))
return context.Merge(localContext);

return AreTokenTypeValidationResultsEqual(
object1 as TokenTypeValidationResult,
object2 as TokenTypeValidationResult,
"TokenTypeValidationResult1",
"TokenTypeValidationResult2",
null,
context);
}

internal static bool AreTokenTypeValidationResultsEqual(
TokenTypeValidationResult tokenTypeValidationResult1,
TokenTypeValidationResult tokenTypeValidationResult2,
string name1,
string name2,
string stackPrefix,
CompareContext context)
{
var localContext = new CompareContext(context);
if (!ContinueCheckingEquality(tokenTypeValidationResult1, tokenTypeValidationResult2, localContext))
return context.Merge(localContext);

if (tokenTypeValidationResult1.Type != tokenTypeValidationResult2.Type)
localContext.Diffs.Add($"TokenTypeValidationResult1.Type: '{tokenTypeValidationResult1.Type}' != TokenTypeValidationResult2.ExpirationTime: '{tokenTypeValidationResult2.Type}'");

if (tokenTypeValidationResult1.IsValid != tokenTypeValidationResult2.IsValid)
localContext.Diffs.Add($"TokenTypeValidationResult1.IsValid: {tokenTypeValidationResult1.IsValid} != TokenTypeValidationResult2.IsValid: {tokenTypeValidationResult2.IsValid}");

if (tokenTypeValidationResult1.ValidationFailureType != tokenTypeValidationResult2.ValidationFailureType)
localContext.Diffs.Add($"TokenTypeValidationResult1.ValidationFailureType: {tokenTypeValidationResult1.ValidationFailureType} != TokenTypeValidationResult2.ValidationFailureType: {tokenTypeValidationResult2.ValidationFailureType}");

// true => both are not null.
if (ContinueCheckingEquality(tokenTypeValidationResult1.Exception, tokenTypeValidationResult2.Exception, localContext))
{
AreStringsEqual(
tokenTypeValidationResult1.Exception.Message,
tokenTypeValidationResult2.Exception.Message,
$"({name1}).Exception.Message",
$"({name2}).Exception.Message",
localContext);

AreStringsEqual(
tokenTypeValidationResult1.Exception.Source,
tokenTypeValidationResult2.Exception.Source,
$"({name1}).Exception.Source",
$"({name2}).Exception.Source",
localContext);

if (!string.IsNullOrEmpty(stackPrefix))
AreStringPrefixesEqual(
tokenTypeValidationResult1.Exception.StackTrace.Trim(),
tokenTypeValidationResult2.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
Loading