JWT authentication with Delphi. Part 4


In the previous three articles I've covered the basics concerning the use of the JWT in Delphi (token generation, signature validation). In this, and in the next articles, I want to show some advanced concepts of using the JWT. A few topics I will cover:

  • Powerful claims validation with the JWT Consumer
  • Use of Custom Claims
  • Best Practices using JWT in REST Services
  • Security consideration (revisited)

JWT Consumer* classes

In a later update I introduced some classes (interfaces) that help the validation process of the JWT's claims. To be clear, in order to validate only the compact token format or the signature you don't need these *Consumer classes, you simply call the TJOSE class-method Verify(), here the signatures of the method:

    class function Verify(AKey: TJWK; const ACompactToken: TJOSEBytes; AClaimsClass: TJWTClaimsClass = nil): TJWT; overload;
    class function Verify(AKey: TJOSEBytes; const ACompactToken: TJOSEBytes; AClaimsClass: TJWTClaimsClass = nil): TJWT; overload;

In a real use case of the JWT token (such as a REST framework) validating the signature is a "sine qua non" condition but, usually, you would like to validate other aspect of the JWT: is it expired? is the subject correct? or perhaps you want to take into account some seconds to avoid a too strict time-based comparison.
For all these scenarios the TJOSEConsumer (and TJOSEConsumerBuilder) will come to your rescue!

JWT validation framework

The Delphi JOSE-JWT library provides a secure framework that takes care of all necessary steps to validate a JWT:

  1. Token parsing: The access token (compact token) is parsed as a JWT. If the parsing fails, then the token is considered invalid.

  2. Signature validation: The digital signature is automatically verified by the library recreating the signature with the payload, the algorithm and the secret kept on the server.

  3. Algorithm checking: The algorithm specified in the JWT header is checked against the expected algorithms, if it doesn't match, then it's considered invalid. This feature prevents downgrade attacks and other attacks against a JWT.

  4. Claims validation: The JWT claims are validated following the RFC's rules. The claims validation phase is fully customizable through anonymous functions.

A first look at the source code to properly validate a JWT token using a TJOSEConsumer:

procedure TfrmConsumer.actBuildJWTConsumerExecute(Sender: TObject);  
begin

  ProcessConsumer(TJOSEConsumerBuilder.NewConsumer

    // Set the appropriate claims class (if needed)
    .SetClaimsClass(TJWTClaims)

    // JWS-related validation
    .SetVerificationKey(edtConsumerSecret.Text)
    .SetSkipVerificationKeyValidation
    .SetDisableRequireSignature

    // string-based claims validation
    .SetExpectedSubject('paolo-rossi')
    .SetExpectedAudience(True, ['Paolo', 'Luca'])

    // Time-related claims validation
    .SetRequireIssuedAt
    .SetRequireExpirationTime
    .SetEvaluationTime(IncSecond(FNow, 26))
    .SetAllowedClockSkew(20, TJOSETimeUnit.Seconds)
    .SetMaxFutureValidity(20, TJOSETimeUnit.Minutes)

    // Build the consumer object
    .Build()
  );
end;  

Token parsing

When you load a compact token in order to validate it, the JOSE-JWT Delphi library parses the compact token and report an error of token malformed if it's not compliant. The checking code recognizes a JWS token (3 parts) and a JWE token (5 parts) although the JOSE-JWT Delphi library doesn't fully implements JWE. The format checking is performed when you assign the compact token representation:

LSigner := TJWS.Create(LToken);  
  try
    LSigner.SetKey(LKey);

    // This will set off the token parse phase
    LSigner.CompactToken := MyCompactToken;

    Result := LSigner.VerifySignature;
  finally
    LSigner.Free;
  end;

Signature validation

The validation of the signature in an integral part of the JOSE-JWT library and is performed using the TJOSE.Verify() method or invoking the TJWS.VerifySignature() method of the JWS class, this last method has two overloaded versions:

    function VerifySignature: Boolean; overload;
    function VerifySignature(AKey: TJWK; const ACompactToken: TJOSEBytes): Boolean; overload;

Algorithm checking

You can specify a list of algorithms in the consumer in order to limit the possibility of a downgrade attack. The checking is performed by the TJOSEConsumer, for example: SetExpectedAlgorithms(TJOSEAlgorithmId.HS256, TJOSEAlgorithmId.RS256)

Claims validation

As I said before in a real situation you (the server) want to validate (some of) the claims that are in the token. Some validation could be: token expiration, token issuer, token audience. With this library you can either check for the presence or for a specific value of the claim.

  // Subject validation
  .SetExpectedSubject('paolo-rossi')

  // Audience validation
  .SetExpectedAudience(True, ['Paolo', 'Luca'])

  // Checks the presence of the iat claim
  .SetRequireIssuedAt

  // Checks the presence of the exp claim
  .SetRequireExpirationTime

  // Sets the evaluation time
  .SetEvaluationTime(IncSecond(FNow, 26))

  // Sets a clock skew for a more relaxed time-based validation
  .SetAllowedClockSkew(20, TJOSETimeUnit.Seconds)

Custom claims validation

The JOSE-JWT library provides a way to validate the standard or private claims through custom code using anonymous methods.
The definition of that anonymous method is the following:

  TJOSEValidator = reference to function (AJOSEContext: TJOSEContext): string;

Calling the TJOSEBuilder.RegisterValidator(MyCustomValidator) method you can register your custom validator that will be called by the TJOSEConsumer in the process phase. Of course the "standard" validators will be called before your(s). The TJOSEBuilder has another method: SetSkipAllDefaultValidators that allows to skip entirely the standard claims validation and (if you want) to replace entirely the validation process with your code.
In your custom validators you will have access to all the JWT "part" through the TJOSEContext object passed to the custom validator method. The TJOSEContext class, as you can see, has the GetHeader and GetClaims methods that will return all you need. The generic GetClaims<T: TJWTClaims> method will give you the claims object of the desired type.

  TJOSEContext = class
  public
    function GetJOSEObject: TJOSEParts; overload;
    function GetJOSEObject<T: TJOSEParts>: T; overload;

    function GetHeader: TJWTHeader;

    function GetClaims: TJWTClaims; overload;
    function GetClaims<T: TJWTClaims>: T; overload;
  end;

Demo time?

In the JWTDemo you can find the tabsheet "JWT Consumer (Claim Validation)" that allows you to play with the TJOSEConsumer, TJOSEConsumerBuilder classes and the TJOSEValidator reference.

Conclusion

In this part we learned how to properly validate a JWT received (in a compact representation) through the TJOSEConsumer and TJOSEConsumerBuilder classes.

Stay tuned for the next part where I will explain in detail the use of custom claims in a JWT token (TJWTClaimsClass class reference).