Skip to content

Commit

Permalink
Lifetime validation: Remove exceptions (#2669)
Browse files Browse the repository at this point in the history
* Added LifetimeValidationResult. Added nullability annotations to ValidationResult
* Added LifetimeValidationResult comparer
* Added ValidateLifetime new version returning LifetimeValidationResult and removing all exception throwing. Updated documentation to disambiguate
* Added ValidateLifetime tests
  • Loading branch information
iNinja authored Jun 29, 2024
1 parent cb39931 commit 24411fd
Show file tree
Hide file tree
Showing 9 changed files with 621 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -992,7 +992,7 @@ protected virtual void ValidateIssuerSecurityKey(SecurityKey securityKey, Securi
/// <param name="expires">The <see cref="DateTime"/> value found in the <see cref="SamlSecurityToken"/>.</param>
/// <param name="securityToken">The <see cref="SamlSecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <remarks><see cref="Validators.ValidateLifetime"/> for additional details.</remarks>
/// <remarks><see cref="Validators.ValidateLifetime(DateTime?, DateTime?, SecurityToken, TokenValidationParameters)"/> for additional details.</remarks>
protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
Validators.ValidateLifetime(notBefore, expires, securityToken, validationParameters);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -335,7 +335,7 @@ protected virtual void ValidateIssuerSecurityKey(SecurityKey key, Saml2SecurityT
/// <param name="expires">The <see cref="DateTime"/> value found in the <see cref="Saml2SecurityToken"/>.</param>
/// <param name="securityToken">The <see cref="Saml2SecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <remarks><see cref="Validators.ValidateLifetime"/> for additional details.</remarks>
/// <remarks><see cref="Validators.ValidateLifetime(DateTime?, DateTime?, SecurityToken, TokenValidationParameters)"/> for additional details.</remarks>
protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
Validators.ValidateLifetime(notBefore, expires, securityToken, validationParameters);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// 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 lifetime 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 LifetimeValidationResult : ValidationResult
{
private Exception? _exception;

/// <summary>
/// Creates an instance of <see cref="LifetimeValidationResult"/>
/// </summary>
/// <paramref name="notBefore"/> is the date from which the token that was validated successfully is valid.
/// <paramref name="expires"/> is the expiration date for the token that was validated successfully.
public LifetimeValidationResult(DateTime? notBefore, DateTime? expires)
: base(ValidationFailureType.ValidationSucceeded)
{
NotBefore = notBefore;
Expires = expires;
IsValid = true;
}

/// <summary>
/// Creates an instance of <see cref="LifetimeValidationResult"/>
/// </summary>
/// <paramref name="notBefore"/> is the date from which the token is valid.
/// <paramref name="expires"/> is the expiration date for the token.
/// <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 LifetimeValidationResult(DateTime? notBefore, DateTime? expires, ValidationFailureType validationFailure, ExceptionDetail exceptionDetail)
: base(validationFailure, exceptionDetail)
{
NotBefore = notBefore;
Expires = expires;
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 SecurityTokenInvalidLifetimeException securityTokenInvalidLifetimeException)
{
securityTokenInvalidLifetimeException.NotBefore = NotBefore;
securityTokenInvalidLifetimeException.Expires = Expires;
securityTokenInvalidLifetimeException.Source = "Microsoft.IdentityModel.Tokens";
}

return _exception;
}
}

/// <summary>
/// Gets the date from which the token is valid.
/// </summary>
public DateTime? NotBefore { get; }

/// <summary>
/// Gets the expiration date for the token.
/// </summary>
public DateTime? Expires { 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 lifetime validation failed.
/// </summary>
public static readonly ValidationFailureType LifetimeValidationFailed = new LifetimeValidationFailure("LifetimeValidationFailure");
private class LifetimeValidationFailure : ValidationFailureType { internal LifetimeValidationFailure(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
Expand Up @@ -5,6 +5,7 @@
using System.Collections.Generic;
using System.Diagnostics;

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
Expand Down Expand Up @@ -49,18 +50,18 @@ protected ValidationResult(ValidationFailureType validationFailureType, Exceptio
/// <param name="stackFrame"></param>
public void AddStackFrame(StackFrame stackFrame)
{
ExceptionDetail.StackFrames.Add(stackFrame);
ExceptionDetail?.StackFrames.Add(stackFrame);
}

/// <summary>
/// Gets the <see cref="Exception"/> that occurred during validation.
/// </summary>
public abstract Exception Exception { get; }
public abstract Exception? Exception { get; }

/// <summary>
/// Gets the <see cref="ExceptionDetail"/> that occurred during validation.
/// </summary>
public ExceptionDetail ExceptionDetail { get; }
public ExceptionDetail? ExceptionDetail { get; }

/// <summary>
/// True if the token was successfully validated, false otherwise.
Expand Down Expand Up @@ -108,3 +109,4 @@ public ValidationFailureType ValidationFailureType
} = ValidationFailureType.ValidationNotEvaluated;
}
}
#nullable disable
157 changes: 157 additions & 0 deletions src/Microsoft.IdentityModel.Tokens/Validation/Validators.Lifetime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,30 @@
// Licensed under the MIT License.

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

#nullable enable
namespace Microsoft.IdentityModel.Tokens
{
/// <summary>
/// Definition for delegate that will validate the lifetime of a <see cref="SecurityToken"/>.
/// </summary>
/// <param name="notBefore">The 'notBefore' time found in the <see cref="SecurityToken"/>.</param>
/// <param name="expires">The 'expiration' time found in the <see cref="SecurityToken"/>.</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>
/// <returns>A <see cref="IssuerValidationResult"/>that contains the results of validating the issuer.</returns>
/// <remarks>This delegate is not expected to throw.</remarks>
internal delegate LifetimeValidationResult LifetimeValidationDelegate(
DateTime? notBefore,
DateTime? expires,
SecurityToken? securityToken,
TokenValidationParameters validationParameters,
CallContext callContext);

/// <summary>
/// IssuerValidation
/// </summary>
Expand Down Expand Up @@ -46,5 +66,142 @@ public static void ValidateLifetime(DateTime? notBefore, DateTime? expires, Secu

ValidatorUtilities.ValidateLifetime(notBefore, expires, securityToken, validationParameters);
}

/// <summary>
/// Validates the lifetime of a <see cref="SecurityToken"/>.
/// </summary>
/// <param name="notBefore">The 'notBefore' time found in the <see cref="SecurityToken"/>.</param>
/// <param name="expires">The 'expiration' time found in the <see cref="SecurityToken"/>.</param>
/// <param name="securityToken">The <see cref="SecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <param name="callContext"></param>
/// <returns>A <see cref="LifetimeValidationResult"/> indicating whether validation was successful, and providing a <see cref="SecurityTokenInvalidLifetimeException"/> if it was not.</returns>
/// <exception cref="ArgumentNullException">If 'validationParameters' is null.</exception>
/// <exception cref="SecurityTokenNoExpirationException">If 'expires.HasValue' is false and <see cref="TokenValidationParameters.RequireExpirationTime"/> is true.</exception>
/// <exception cref="SecurityTokenInvalidLifetimeException">If 'notBefore' is &gt; 'expires'.</exception>
/// <exception cref="SecurityTokenNotYetValidException">If 'notBefore' is &gt; DateTime.UtcNow.</exception>
/// <exception cref="SecurityTokenExpiredException">If 'expires' is &lt; DateTime.UtcNow.</exception>
/// <remarks>All time comparisons apply <see cref="TokenValidationParameters.ClockSkew"/>.</remarks>
/// <remarks>Exceptions are not thrown, but embedded in <see cref="LifetimeValidationResult.Exception"/>.</remarks>
#pragma warning disable CA1801 // TODO: remove pragma disable once callContext is used for logging
internal static LifetimeValidationResult ValidateLifetime(DateTime? notBefore, DateTime? expires, SecurityToken? securityToken, TokenValidationParameters validationParameters, CallContext callContext)
#pragma warning restore CA1801
{
if (validationParameters == null)
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.NullArgument,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10000,
LogHelper.MarkAsNonPII(nameof(validationParameters))),
typeof(ArgumentNullException),
new StackFrame(true)));

if (validationParameters.LifetimeValidator != null)
return ValidateLifetimeUsingDelegate(notBefore, expires, securityToken, validationParameters);

if (!validationParameters.ValidateLifetime)
{
LogHelper.LogInformation(LogMessages.IDX10238);
return new LifetimeValidationResult(notBefore, expires);
}

if (!expires.HasValue && validationParameters.RequireExpirationTime)
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10225,
LogHelper.MarkAsNonPII(securityToken == null ? "null" : securityToken.GetType().ToString())),
typeof(SecurityTokenNoExpirationException),
new StackFrame(true)));

if (notBefore.HasValue && expires.HasValue && (notBefore.Value > expires.Value))
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10224,
LogHelper.MarkAsNonPII(notBefore.Value),
LogHelper.MarkAsNonPII(expires.Value)),
typeof(SecurityTokenInvalidLifetimeException),
new StackFrame(true)));

DateTime utcNow = DateTime.UtcNow;
if (notBefore.HasValue && (notBefore.Value > DateTimeUtil.Add(utcNow, validationParameters.ClockSkew)))
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10222,
LogHelper.MarkAsNonPII(notBefore.Value),
LogHelper.MarkAsNonPII(utcNow)),
typeof(SecurityTokenNotYetValidException),
new StackFrame(true)));

if (expires.HasValue && (expires.Value < DateTimeUtil.Add(utcNow, validationParameters.ClockSkew.Negate())))
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10223,
LogHelper.MarkAsNonPII(expires.Value),
LogHelper.MarkAsNonPII(utcNow)),
typeof(SecurityTokenExpiredException),
new StackFrame(true)));

// if it reaches here, that means lifetime of the token is valid
if (LogHelper.IsEnabled(EventLogLevel.Informational))
LogHelper.LogInformation(LogMessages.IDX10239);

return new LifetimeValidationResult(notBefore, expires);
}

private static LifetimeValidationResult ValidateLifetimeUsingDelegate(DateTime? notBefore, DateTime? expires, SecurityToken? securityToken, TokenValidationParameters validationParameters)
{
try
{
if (!validationParameters.LifetimeValidator(notBefore, expires, securityToken, validationParameters))
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10230,
securityToken),
typeof(SecurityTokenInvalidLifetimeException),
new StackFrame(true)));

return new LifetimeValidationResult(notBefore, expires);
}
#pragma warning disable CA1031 // Do not catch general exception types
catch (Exception delegateException)
#pragma warning restore CA1031 // Do not catch general exception types
{
return new LifetimeValidationResult(
notBefore,
expires,
ValidationFailureType.LifetimeValidationFailed,
new ExceptionDetail(
new MessageDetail(
LogMessages.IDX10230,
securityToken),
delegateException.GetType(),
new StackFrame(true),
delegateException));
}
}
}
}
#nullable restore
Original file line number Diff line number Diff line change
Expand Up @@ -1674,7 +1674,7 @@ protected virtual void ValidateAudience(IEnumerable<string> audiences, JwtSecuri
/// <param name="expires">The <see cref="DateTime"/> value of the 'exp' claim if it exists in the 'jwtToken'.</param>
/// <param name="jwtToken">The <see cref="JwtSecurityToken"/> being validated.</param>
/// <param name="validationParameters"><see cref="TokenValidationParameters"/> required for validation.</param>
/// <remarks><see cref="Validators.ValidateLifetime"/> for additional details.</remarks>
/// <remarks><see cref="Validators.ValidateLifetime(DateTime?, DateTime?, SecurityToken, TokenValidationParameters)"/> for additional details.</remarks>
protected virtual void ValidateLifetime(DateTime? notBefore, DateTime? expires, JwtSecurityToken jwtToken, TokenValidationParameters validationParameters)
{
Validators.ValidateLifetime(notBefore, expires, jwtToken, validationParameters);
Expand Down
Loading

0 comments on commit 24411fd

Please sign in to comment.