JWT authentication with Delphi. Part 3


Now that we have introduced the JSON Web Token in Part 1 and dissected it in Part 2, we are ready to fire up Delphi and start writing some code to generate, verify, and validate some JWT tokens.

Cryptographic key considerations

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  "secret"
)

Nearly all JWT's examples (even mines) use the word 'secret' as the secret key to sign the token but this is problematic because it is too short for the HS256 algorithm (or HS384 or HS512) so it's quite ineffective, in fact this can be quite dangerous from a security perspective.

Speaking of cryptographic hash functions (HMACxxx) we can set up some general rules: given a hash function H with a block size B and a byte-length of hash output L we can say that (RFC 2104):

The key for HMAC can be of any length (keys longer than B bytes are first hashed using H). However, less than L bytes is strongly discouraged as it would decrease the security strength of the function. Keys longer than L bytes are acceptable but the extra length would not significantly increase the function strength. (A longer key may be advisable if the randomness of the key is considered weak)

Bottom line: Please change your signing key accordingly to your security needs!

Enough! let me see the code!

jwt-demo-debugger

Building a signed JWT (JWS)

Using the delphi-jose-jwt library, the creation of the JWT is basically a three-step process:

  1. The first step is the definition of the token's standard claims like Subject, Expiration, JWT ID, and Issuer
  2. The second step is the cryptographic signing of the JWT (JWS)
  3. The final step is the JWT conversion to a URL-safe string, according to the JOSE rules

As you well know, the resulting JWT will be a base64-encoded string divided in 3 parts and signed with the specified key and signature algorithm. After that, you can take the token and (for example) send it to a REST client.

This is a full example that shows the construction of a JWT using the proper JOSE objects. Keep in mind that this is the powerful but "complex" way to build the JWT token but don't worry, the library exposes the TJOSE static class that greatly simplify the construction of the JWT token (we'll see it later).

procedure TfrmSimple.btnBuildClick(Sender: TObject);
var
  LToken: TJWT;
  LSigner: TJWS;
  LKey: TJWK;
  LAlg: TJOSEAlgorithmId;
begin
  LToken := TJWT.Create;
  try
    LToken.Claims.Issuer := 'Delphi JOSE Library';
    LToken.Claims.IssuedAt := Now;
    LToken.Claims.Expiration := Now + 1;

    // Signing algorithm
    case cbbAlgorithm.ItemIndex of
      0: LAlg := TJOSEAlgorithmId.HS256;
      1: LAlg := TJOSEAlgorithmId.HS384;
      2: LAlg := TJOSEAlgorithmId.HS512;
    else LAlg := TJOSEAlgorithmId.HS256;
    end;

    LSigner := TJWS.Create(LToken);
    try
      LKey := TJWK.Create(edtSecret.Text);
      try
        // With this option you can have keys < algorithm length
        LSigner.SkipKeyValidation := True;

        LSigner.Sign(LKey, LAlg);

        memoJSON.Lines.Add('Header: ' + TJSONUtils.ToJSON(LToken.Header.JSON));
        memoJSON.Lines.Add('Claims: ' + TJSONUtils.ToJSON(LToken.Claims.JSON));

        memoCompact.Lines.Add('Header: ' + LSigner.Header);
        memoCompact.Lines.Add('Payload: ' + LSigner.Payload);
        memoCompact.Lines.Add('Signature: ' + LSigner.Signature);
        memoCompact.Lines.Add('Compact Token: ' + LSigner.CompactToken);
      finally
        LKey.Free;
      end;
    finally
      LSigner.Free;
    end;
  finally
    LToken.Free;
  end;
end;

You can see the whole project in the "sample" directory on GitHub, now let's analyze in detail this code. The Delphi classes used to build a JWT token are:

var
  LToken: TJWT;
  LSigner: TJWS;
  LKey: TJWK;
  LAlg: TJOSEAlgorithmId;

The TJWT class represent the token (filled with the claims), the TJWK represent the key used to sign the token, the TJWS it's the class that does the signing, and the TJOSEAlgorithmId is the type (enum type) of the algorithm used, in short:

The TJWS signs the TJWT's claims using the TJWK key with the TJOSEAlgorithmId algorithm

  LToken := TJWT.Create;

  // Here you can fill all of your claims
  LToken.Claims.Issuer := 'Delphi JOSE Library';
  LToken.Claims.IssuedAt := Now;
  LToken.Claims.Expiration := Now + 1;

In order to have a signed token we must create the token and fill the appropriate claims (keep in mind that you can also use a derived class from the TJWTClaims base class)

  // Here you choose the signing algorithm
  case cbbAlgorithm.ItemIndex of
    0: LAlg := TJOSEAlgorithmId.HS256;
    1: LAlg := TJOSEAlgorithmId.HS384;
    2: LAlg := TJOSEAlgorithmId.HS512;
  else LAlg := TJOSEAlgorithmId.HS256;
  end;

With the code above we simply choose the algorithm used to create the digital signature of our token.

  // To actually sign the token we create a TJWS object
  LSigner := TJWS.Create(LToken);

  // Create the key object
  LKey := TJWK.Create(edtSecret.Text);

  // With this option you can have keys < algorithm length
  LSigner.SkipKeyValidation := True;

  // Sign the JWT with the chosen key and algorithm
  LSigner.Sign(LKey, LAlg);

And here is the code that does the actual signing and conversion into a compact representation of the JWT:

  1. You have to create a TJWK object (the key) and a TJWS object (the signer)
  2. Then you call the Sign() method to actually sign the JWT (with the key and algorithm as parameters)

The LSigner.SkipKeyValidation := True line is telling the TJWS class that it can skip the validation of the provided key. The validation routine validates against null and (too) short keys.

  // Header: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
  LSigner.Header

  // Payload: eyJpc3MiOiJEZWxwaGkgSk9TRSBMaWJyYXJ5IiwiaWF0IjoxNTM0Nzc5NzQxLCJleHAiOjE1MzQ4NjYxNDF9
  LSigner.Payload

  // Signature: kevr4S3GBixywzvlZ0L9ZRRJb6osJ5WAiEATu6fuAK8
  LSigner.Signature

  // Compact Token: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJEZWxwaGkgSk9TRSBMaWJyYXJ5IiwiaWF0IjoxNTM0Nzc5NzQxLCJleHAiOjE1MzQ4NjYxNDF9.kevr4S3GBixywzvlZ0L9ZRRJb6osJ5WAiEATu6fuAK8
  LSigner.CompactToken

After that the JWS is ready and you can access the signed token in a compact representation and send it (for example) to a REST client.

Now that we saw the "right" way to build a signed token (JWS) I can show you the TJOSE class that simplifies the process but, keep in mind, that if you need extra control over the creation of your token you can always use the native JOSE classes.

procedure TfrmSimple.BuildJWT;
var
  LToken: TJWT;
begin
  // Create a JWT Object
  LToken := TJWT.Create;
  try
    // Token claims
    LToken.Claims.Subject := 'Paolo Rossi';
    LToken.Claims.IssuedAt := Now;
    LToken.Claims.Expiration := Now + 1;
    LToken.Claims.Issuer := 'Delphi JOSE Library';

    // Signing and compact format creation.
    // Please use a different secret key in production!!!!!
    memoCompact.Lines.Add(TJOSE.SHA256CompactToken('mysecretkey', LToken));
  finally
    LToken.Free;
  end;
end;

As you can see after building your JWT with the needed claims, to get the signed token you only have to call TJOSE.SHA256CompactToken('secret', LToken). Pretty simple right?

Obviously there are other utility methods such as SHA384CompactToken() and SHA512CompactToken() to cover other key lengths. The only caveat here is that using the SHA* methods the SkipKeyValidation property of the TJWS is always set to True.

Decode and verify tokens

The process of verifying a compact token is very simple using the delphi-jose-jwt library but before showing the code I want to clarify some (possible) confusion about the token verification and validation.

Verification vs Validation

In order to ensure that the message (payload) wasn't changed along the way, we have to verify the signature of the token.

After a token (the signature) is verified we can validate its claims so, for example, we can check if the given token is expired or if the subject it's the expected subject, etc...

As you can see the most important thing to do when you receive a token is to verify it because if the signature has been forged or the payload has been tampered with you cannot trust the claims, therefore there is no point in validating them.

Token verification

The verification is a very straightforward process. As usual I will show you the complete snippet using the JOSE classes first:

procedure TfrmSimple.VerifyTokenComplete;
var
  LKey: TJWK;
  LToken: TJWT;
  LSigner: TJWS;
begin
  LKey := TJWK.Create(edtSecret.Text);
  try
    LToken := TJWT.Create;
    try
      LSigner := TJWS.Create(LToken);
      try
        LSigner.SetKey(LKey);
        LSigner.CompactToken := FCompact;

        if LSigner.VerifySignature then
          memoJSON.Lines.Add('Token signature is verified')
        else
          memoJSON.Lines.Add('Token signature is not verified')
      finally
        LSigner.Free;
      end;
    finally
      LToken.Free;
    end;
  finally
    LKey.Free;
  end;
end;

You have to create the TJWK, TJWT, TJWS objects, then you have to set the Key object and the compact token for the LSigner object: LSigner.SetKey(LKey) and LSigner.CompactToken := FCompact. Now you can call the VerifySignature() method and check if the given compact token is a valid token, only after that you can safely access to the token's claims.

procedure TfrmSimple.VerifyToken;
var
  LToken: TJWT;
begin
  // Unpack and verify the token
  LToken := TJOSE.Verify(edtSecret.Text, FCompact);

  if Assigned(LToken) then
  begin
    try
      if LToken.Verified then
        memoJSON.Lines.Add('Token signature verified, you can trust the claims')
      else
        memoJSON.Lines.Add('Token signature not verified')
    finally
      LToken.Free;
    end;
  end;
end;

To further simplify the verification task, you can use the TJOSE utility class. The TJOSE.Verify() method takes the key and the compact token representation and returns a TJWT object but, remember, that you have to check if the the TJWT itself is verified before accessing its claims.

Claims validation

The validation of the token's claims cannot be fully "automated" because it's the user choice (and responsibility) to set the claims and to check them.

The JWT standard (RFC 7519) defines some "standard" claims as I explained in Part 2. In a "real" situation some claims are very important, like the Expiration Time exp, the Subject sub or the Issued At iat, etc... So when you receive back the token, after the signature verification you must also validate the claims found in the token.

  if LToken.Verified then
  begin
    // Claims validation (for more see the JOSE.Consumer unit)

    if LToken.Claims.Subject <> 'Paolo Rossi' then
      memoJSON.Lines.Add('Subject [sub] claim value doesn''t match expected value');

    if LToken.Claims.Expiration > Now then
      memoJSON.Lines.Add('Expiration time passed: ' + DateTimeToStr(LToken.Claims.Expiration));
  end;

This is only an example of claims validation but you can, of course, add your own logic to the validation process. In addition you can also create JWTs with custom claims and therefore you have to do some custom validations.

That said, the delphi-jose-jwt library contains some classes that greatly simplify the claims validation process. You can find the classes in the JOSE.Consumer.pas and JOSE.Consumer.Validators.pas units (more on this in the next article)

jwt-demo-consumer

Conclusion

In this part we have described how to build, decode, verify and validate a JWT token. In the next part I will explain in detail the use of custom claims in a JWT token (TJWTClaimsClass class reference) and the JOSE.Consumer.* classes to validate the standard claims that strictly follow the JWT standard.