Skip to content

ValidatingTokens

Tim Hannifin edited this page Jun 2, 2023 · 30 revisions

Token Validation has multiple parts. The token is validated by checking that it is for the application, that it was issued by a trusted Identity Provider (IDP), that the token's lifetime is in range, and that it was not tampered with. There can also be special validations. For instance, the signing keys that validated the token need additional validation to be trusted and that the token is not being replayed. Finally, some protocols require specific validations.

Dynamically obtaining SecurityKey(s) for validating signatures

Normally we obtain SecurityKeys for you by reaching out to the IDP (Discovery or Metadata). Sometimes it is necessary to obtain keys dynamically at runtime. Perhaps they are in a database, cached somewhere or Mutual TLS is being used. Here is how to do that. Dynamically obtaining signing keys at runtime

Validators

The validation steps are captured into Validators, which are all in one source file: Microsoft.IdentityModel.Tokens/Validators.cs

The validators are the following:

Validator Description
ValidateAudience Ensures that the token is indeed for the application that validates the token (for me)
ValidateIssuer Ensures that the token was issued by a STS I trust (from someone I TRUST)
ValidateIssuerSigningKey Ensures the application validating the token trusts the key that was used to sign the token (this is a special case where the key is embedded in the token, usually this is not required)
ValidateLifetime Ensures that the token is still (or already) valid. This is done by checking that the lifetime of the token (notbefore, expires) is in range
ValidateTokenReplay Ensure the token is not replayed (this is a special case for some onetime use protocols)

Setting RequireExpirationTime or RequireAudience to false on the TokenValidationParameters will allow for tokens which don't meet minimum security recommendations and therefore it is not recommended to do so. Instead of turning off RequireExpirationTime, LifetimeValidator can be used to take control of validation in more complex scenarios for token expiry.

Setting ValidateAudience, ValidateIssuer, or ValidateLifetime to false disables critical validation checks and will result in an insecure pattern for validation. If the default validation for these fields doesn't suit your scenario, please make use of the respective delegate overrides (AudienceValidator, IssuerValidator, or LifetimeValidator respectively). See notes on custom validators below, especially regarding the importance of negative testing as overriding the validator means you're disabling default validation and taking full control of--and responsibility for--that aspect of validation.

Protocol specific validators

In addition to these validators, there are protocol specific validation rules. For example, OpenIdConnect requires the audience (‘aud’) claim to exist. See:Microsoft.IdentityModel.Protocols.OpenIdConnect/OpenIdConnectProtocolValidator.cs#L382

JsonWebTokenHandler

Pattern for using the JsonWebTokenHandler

var jsonWebTokenHandler = new JsonWebTokenHandler();
var tokenValidationResult = jsonWebTokenHandler.ValidateToken(token, validationParameters);
if (!tokenValidationResult.IsValid)
{
   // Handle each exception which tokenValidationResult can contain as appropriate for your service
   // Your service might need to respond with a http response instead of an exception.
   if (tokenValidationResult.Exception != null)
       throw tokenValidationResult.Exception;
    
   throw CustomException("");
}

NOTE: If you are migrating from JwtSecurityTokenHandler, JsonWebTokenHandler does not use exceptions to raise validation issues, the IsValid property must be checked instead. Failure to do so will result in gaps that will compromise the security of your service.

Negative testing with unit test and/or E2E testing

The TokenValidationResult.Exception property contains the actual exception, if one was generated. Your code should ensure the following tests are performed by either unit tests or integration tests which runs part of your build and release pipeline.

You must demonstrate that your application will fault for the following scenarios when using the JsonWebTokenHandler.ValidateToken by checking the exception property of the TokenValidationResult.

The simplest test to perform is:
* SecurityTokenInvalidSignatureException (Signature validation failed)

Additionally your tests should also cover these tests below:
* SecurityTokenExpiredException (Token expired)
* SecurityTokenInvalidAudienceException (Audience validation failed)
* SecurityTokenInvalidIssuerException (Issuer validation failed)
* SecurityTokenSignatureKeyNotFoundException (There is no key in metadata available to validate the token)

There are many more checks to do but these are the minimum to ensure you have negative testing for.

Note: SecurityTokenValidationException is the base exception

Custom validators

The TokenValidationParameters class gives you the option of providing validation delegates for a variety of properties on the token. By implementing the validation delegate and setting it on the TokenValidationParameters, you can add in custom validation logic (e.g. custom lifetime validation).

Important

  • If writing custom validators you are responsible for ensuring the code fails correctly!
  • You should always ensure that you do proper negative testing. Failing to properly validate a token means you will potentially allow people into your service which are not intended to have access.
  • Custom validators should be careful not to trust data which has not been validated, there are known claims misuse attacks which can be a vector for command injection that creators of custom validators are taking on board. See this example with 'kid' based command injection.

The validation delegates present on TokenValidationParameters are:

public delegate SecurityToken SignatureValidator(string token, TokenValidationParameters validationParameters);
public delegate bool TokenReplayValidator(DateTime? expirationTime, string securityToken, TokenValidationParameters validationParameters);
public delegate string TypeValidator(string type, SecurityToken securityToken, TokenValidationParameters validationParameters);
public delegate bool AlgorithmValidator(string algorithm, SecurityKey securityKey, SecurityToken securityToken, TokenValidationParameters validationParameters);
public delegate bool AudienceValidator(IEnumerable<string> audiences, SecurityToken securityToken, TokenValidationParameters validationParameters);
public delegate string IssuerValidator(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters);
public delegate bool LifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken securityToken, TokenValidationParameters validationParameters);

Example of a custom lifetime validation delegate:

public static bool CustomLifetimeValidator(DateTime? notBefore, DateTime? expires, SecurityToken token, TokenValidationParameters validationParameters)
{
     if (...)
         return true;
     else (...)
         return false;
}

public static string CustomIssuerValidator(string issuer, SecurityToken securityToken, TokenValidationParameters validationParameters)
{
     if (!MyCustomIssuerValidationCheck(issuer))
         throw MySecurityTokenInvalidIssuerException;

     ...
     
     return issuer;
}

Setting the validation delegate:

var tokenValidationParameters = new TokenValidationParameters();
tokenValidationParameters.LifetimeValidator = CustomLifetimeValidator;
tokenValidationParameters.IssuerValidator = CustomIssuerValidator;

IMPORTANT:

If a validation delegate is provided, it is responsible for ALL validation involving the property in question. The library will NOT perform any additional checks. This also means throwing an exception if the validation should fail in some cases:

Validation Delegate Exception Behavior
AudienceValidator A SecurityTokenInvalidAudienceException is thrown if the delegate returns false.
AlgorithmValidator A SecurityTokenInvalidAlgorithmException is thrown if the delegate returns false.
IssuerValidator No exception is thrown by default, the delegate is responsible for returning the issuer to use. A SecurityTokenInvalidIssuerException should be thrown by the delegate in the failure case.
IssuerSigningKeyValidator A SecurityTokenInvalidSigningKeyException is thrown if the delegate returns false.
LifetimeValidator A SecurityTokenInvalidLifetimeException is thrown if the delegate returns false.
TokenReplayValidator A SecurityTokenReplayDetectedException is thrown if the delegate returns false.
TypeValidator No exception is thrown by default, the delegate is responsible for returning the actual token type. A SecurityTokenInvalidTypeException should be thrown by the delegate in the failure case.

DX10205 Issuer validation failed

If you received this error message

IDX10205: Issuer validation failed because the actual issuer didn't match the valid issuer(s). Issuer: 'System.String (value removed)'. Did not match: validationParameters.ValidIssuer: 'System.String (value removed)' or validationParameters.ValidIssuers: 'System.String (value removed)'

you will want to first enable PII to see the values removed from the message. Please enable PII logs to see them. See https://aka.ms/IdentityModel/PII for details on enabling PII logs. NOTE: this is only required for older versions of the library, newer versions will display this info without requiring PII to be enabled as the issuer is not considered PII.

To resolve the issue, make sure the correct issuer is specified in the ValidIssuer or ValidIssuers or IssuerValidator property of TokenValidationParameters. The issuer is checked based on one value coming from the OIDC document, based on the provided authority in the configuration, and the other value is coming from the token.

AadIssuerValidator

For instance, in ASP.NET Core:

// For a web app
services.Configure<OpenIdConnectOptions>(OpenIdConnectDefaults.AuthenticationScheme,
      options =>
      {
           options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(
                   options.Authority,
                   options.Backchannel).Validate;
      });
// For a web API: 
services.Configure<OpenIdConnectOptions>(JwtBearerDefaults.AuthenticationScheme,
      options =>
      {
           options.TokenValidationParameters.IssuerValidator = AadIssuerValidator.GetAadIssuerValidator(
                   options.Authority).Validate;
      });
Clone this wiki locally