-
Notifications
You must be signed in to change notification settings - Fork 415
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
Updated documentation for the new validation model and restructured internals #3056
base: dev
Are you sure you want to change the base?
Conversation
… of GetCurrentStackFrame() and AddCurrentStackFrame()
…fields onto their own files and made the structures read-only.
src/Microsoft.IdentityModel.Tokens/Validation/Validators.IssuerSigningKey.cs
Show resolved
Hide resolved
…g IList values from two-part constructors.
… no ActorValidationParameters are provided.
…once the classes/structures are made public.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copilot reviewed 28 out of 43 changed files in this pull request and generated no comments.
Files not reviewed (15)
- src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt: Language not supported
- src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.StackFrames.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.StackFrames.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.StackFrames.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Saml2SecurityTokenHandler.ValidateToken.Internal.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerSigningKeyValidationError.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AudienceValidationError.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens.Saml/Saml/SamlSecurityTokenHandler.ValidateToken.Internal.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/MessageDetail.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.JsonWebTokens/JwtTokenUtilities.DecryptTokenResult.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.DecryptToken.cs: Evaluated as low risk
- src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ReadToken.cs: Evaluated as low risk
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM
I've asked a few questions, which I'd like to be addressed, and proposed to improve the error messages to make them more actionable.
src/Microsoft.IdentityModel.Tokens.Saml/Saml/Exceptions/SamlValidationError.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens.Saml/Saml2/Exceptions/Saml2ValidationError.cs
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AudienceValidationError.cs
Show resolved
Hide resolved
...Microsoft.IdentityModel.Tokens/Validation/Results/Details/IssuerSigningKeyValidationError.cs
Show resolved
Hide resolved
/// <param name="securityToken">The <see cref="SecurityToken"/> that is being validated.</param> | ||
/// <param name="tokenHandler">The <see cref="TokenHandler"/> that is being used to validate the token.</param> | ||
/// <param name="validationParameters">The <see cref="ValidationParameters"/> to be used for validating the token.</param> | ||
internal class ValidatedToken( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is it ValidatedToken? Or ValidatedTokenResult?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I consider a "success object", in the sense that it is only ever created and returned as part of a successful validation. The ValidationResult
can be either positive or negative, but the ValidatedToken
is just that, a token that was successfully validated.
@@ -233,8 +213,7 @@ await ValidateJWSAsync(decryptedToken!, validationParameters, configuration, cal | |||
|
|||
if (!validationResult.IsValid) | |||
{ | |||
StackFrame validationFailureStackFrame = StackFrames.JWEValidationFailed ??= new StackFrame(true); | |||
return validationResult.UnwrapError().AddStackFrame(validationFailureStackFrame); | |||
return validationResult.UnwrapError().AddCurrentStackFrame(); | |||
} | |||
|
|||
JsonWebToken jsonWebToken = (validationResult.UnwrapResult().SecurityToken as JsonWebToken)!; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
line 248, can we do better (more precise?) than catching the general Exception class?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, given this path is not meant to throw exceptions and we do not do it within the default validators, this is more of a safety net for the cases where a customer implements one of the delegates and decides to throw an exception.
Given that in that situation we have no way of knowing or limiting the types of exceptions that could occur, this seems like our safest approach.
In a basic scenario where no delegates are overwritten, this catch would never execute.
return ValidationError.NullParameter( | ||
nameof(jwtToken), | ||
tokenNullStackFrame); | ||
ValidationError.GetCurrentStackFrame()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidationError.GetCurrentStackFrame()); | |
ValidationError.GetCurrentStackFrame(), | |
"The JWT token provided is null. Ensure that a valid token is passed."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed this in the other similar suggestion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest 'JsonWebToken' instead of "JWT'
return ValidationError.NullParameter( | ||
nameof(validationParameters), | ||
validationParametersNullStackFrame); | ||
ValidationError.GetCurrentStackFrame()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ValidationError.GetCurrentStackFrame()); | |
ValidationError.GetCurrentStackFrame() | |
"Validation parameters are missing. Provide the necessary parameters for token validation."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently, the NullParameter
static method is a bit of a shortcut for a ValidationError
with ValidationFailureType of NullArgument
, and a message of IDX10000: The parameter '{0}' cannot be a 'null' or an empty object.
.
I see two options here if we really want to add this information:
- We create a new IDX to allow the inclusion of further explanation
- We allow to override the IDX10000 message with this
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest 'ValidationParameters' instead of 'Validation parameters'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will not the current error message along with the stack trace will provide sufficient information to for the user?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should not be assuming the stack traces will be normally seen, as we are aiming to not generate the exceptions unless there is a problem that is being diagnosed.
It may be worth allowing adding a little context along with the X parameter cannot be null or empty
to make it easier to go without the stack trace and make that more of a last resort.
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
src/Microsoft.IdentityModel.JsonWebTokens/JsonWebTokenHandler.ValidateToken.Internal.cs
Outdated
Show resolved
Hide resolved
@@ -200,8 +183,7 @@ await ValidateJWEAsync(jsonWebToken, validationParameters, lkgConfiguration, cal | |||
} | |||
|
|||
// If we reach this point, the token validation failed and we should return the error. | |||
StackFrame stackFrame = StackFrames.TokenValidationFailed ??= new StackFrame(true); | |||
return result.UnwrapError().AddStackFrame(stackFrame); | |||
return result.UnwrapError().AddCurrentStackFrame(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If there no Error, UnWrapError() will throw an InvalidOperationException.
This may not be valuable to the user.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is not possible to reach this line without an error.
ValidationError can only be constructed with a successful result or an error, but always one of the two is present.
The code above this line returns the success value if the result is valid, only reaching this point if it is not, and therefore holding the error.
@@ -7,9 +7,20 @@ | |||
#nullable enable | |||
namespace Microsoft.IdentityModel.Tokens.Saml | |||
{ | |||
/// <summary> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am curious why we need this class.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was introduced as part of the intention to specialise the returned errors from the delegates, though there is a chance that the SAML and SAML2 validation errors could be left as ValidationError and keep the others for the common validations. I'll have a look.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Took me a moment to remember the exact reasoning, but here it is and it is simple:
We have 2 exceptions thrown by the SAMLSecurityTokenHandler and SAML2SecurityHandler that need to be created, each of them deriving from a SamlSecurityTokenException or Saml2SecurityTokenException.
If we try to create those exceptions within ValidationError
using the current approach, we would need to have M.IM.Tokens
depend on the SAML and SAML2 packages.
We implement these two validation errors in order to have each create their custom exception.
/// <param name="exceptionType"/> is the type of exception that occurred. | ||
/// <param name="stackFrame"/> is the stack frame where the exception occurred. | ||
/// <param name="innerException"/> is the inner exception that occurred. | ||
public Saml2ValidationError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Curious why we need this class.
As I don't see a JwtValidationError.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Addressed on the SAML validation error comment.
@@ -7,8 +7,20 @@ | |||
#nullable enable | |||
namespace Microsoft.IdentityModel.Tokens | |||
{ | |||
/// <summary> | |||
/// Represents an algorithm validation error. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Suggest: When validating a SecurityToken the user can specify which algorithms are acceptable. AlgorithmValidationError contains details of why the validation of the SecurityToken failed associated with the algorithm found in the SecurityToken.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This kind of explanation sounds too verbose to me and more suited for a KB article / wiki.
I'll try to expand the current description to something more clear.
src/Microsoft.IdentityModel.Tokens/Validation/Results/Details/AlgorithmValidationError.cs
Outdated
Show resolved
Hide resolved
/// <param name="stackFrame"/> is the stack frame where the exception occurred. | ||
/// <param name="invalidTokenType"/> is the token type that could not be validated. | ||
/// <param name="innerException"/> is the inner exception that occurred. | ||
public TokenTypeValidationError( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general we are not consistent with our default parameters.
invalidTokenType can be null, but we don't have default = null, for innerException we do have a default.
default parameters limit how class methods can be modified in the future.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are two different cases and the default value intends to reflect that.
We want the invalidTokenType
to be present, but because the value on the token itself may be missing, we have to allow null
to be passed here.
The inner exception, on the other hand, is an optional parameter that can be added if present but is not required.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
but one important to have is the cancellationToken = default)
as the last parameter of Async methods
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am adding the default on the main method.
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedIssuer.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedToken.cs
Outdated
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedToken.cs
Outdated
Show resolved
Hide resolved
public string? ValidatedAudience { get; internal set; } | ||
|
||
/// <summary> | ||
/// The issuer that was validated. If present, it contains the source of the validation as well. | ||
/// </summary> | ||
public ValidatedIssuer? ValidatedIssuer { get; internal set; } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is the idea that the 'internal set' will become public so the user can set the value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ideal would be to make these properties be { get; init; }
to make them only settable from the two-part constructor when calling:
var validatedToken = new ValidatedToken(token, handler, parameters)
{
ValidatedIssuer = someValidatedIssuer,
ValidatedLifetime = someValidatedLifetime,
}
and outside of that, keep the property as read-only.
There is a small incompatibility between the init
keyword and .NET Framework / .NET Standard that requires a workaround that is not included here as it requires its own conversation.
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
src/Microsoft.IdentityModel.Tokens/Validation/Results/IssuerValidationSource.cs
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedIssuer.cs
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedLifetime.cs
Show resolved
Hide resolved
src/Microsoft.IdentityModel.Tokens/Validation/Results/ValidatedSigningKeyLifetime.cs
Show resolved
Hide resolved
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LTGM
Thanks @iNinja
# Conflicts: # src/Microsoft.IdentityModel.Tokens/InternalAPI.Unshipped.txt
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
…r the new validation model
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
SummarySummary
CoverageMicrosoft.IdentityModel.JsonWebTokens - 80.3%
|
/// The validated issuer's string representation. | ||
/// </summary> | ||
/// <returns>A string representing the issuer and where it was validated from.</returns> | ||
public override string ToString() => $"{Issuer} (from {ValidationSource})"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we cache this so we don't create a new string each time? (similar comment for other files in this commit)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just one question, otherwise LGTM
Updated documentation for the new validation model and restructured internals
Clean up work and documentation updates for the new validation model.
GetCurrentStackFrame()
andAddCurrentStackFrame()
IssuerValidationSource
to be extensible.Part of #2711