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

Lifetime validation: Remove exceptions #2669

Merged
merged 5 commits into from
Jun 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
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)
keegan-caruso marked this conversation as resolved.
Show resolved Hide resolved
: 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>
keegan-caruso marked this conversation as resolved.
Show resolved Hide resolved
/// <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>
keegan-caruso marked this conversation as resolved.
Show resolved Hide resolved
/// <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