JWT Authentication with Delphi Series
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
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
HMACcan be of any length (keys longer than
Bbytes are first hashed using
H). However, less than
Lbytes is strongly discouraged as it would decrease the security strength of the function. Keys longer than
Lbytes 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!
Building a signed JWT (JWS)
Using the delphi-jose-jwt library, the creation of the JWT is basically a three-step process:
- The first step is the definition of the token's standard claims like Subject, Expiration, JWT ID, and Issuer
- The second step is the cryptographic signing of the JWT (JWS)
- 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;
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:
TJWT's claims using the
TJWKkey with the
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:
- You have to create a
TJWKobject (the key) and a
TJWSobject (the signer)
- Then you call the
Sign()method to actually sign the JWT (with the key and algorithm as parameters)
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
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
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.
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
TJWS objects, then you have to set the Key object and the compact token for the LSigner object:
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.
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.Validators.pas units (more on this in the next article)
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.