Skip to content

Commit

Permalink
Token Type validation: Remove exceptions (#2688)
Browse files Browse the repository at this point in the history
* Remove throws when validating Token Type.

* Clean-up

* Addressing Feedback

---------

Co-authored-by: Franco Fung <francofung@microsoft.com>
  • Loading branch information
FuPingFranco and Franco Fung authored Jul 9, 2024
1 parent 2062d42 commit 1dd1e98
Show file tree
Hide file tree
Showing 7 changed files with 483 additions and 1 deletion.
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
118 changes: 118 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/Validation/Validators.TokenType.cs
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

0 comments on commit 1dd1e98

Please sign in to comment.