JWT authentication with Delphi. Part 3
JWT Authentication with Delphi Series
- Part 1: Authorization and JWT basic concepts
- Part 2: The JWT in depth
- Part 3: Building and verifying JWTs in Delphi
- Part 4: Using the Consumer to validate the JWT
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 thanB
bytes are first hashed usingH
). However, less thanL
bytes is strongly discouraged as it would decrease the security strength of the function. Keys longer thanL
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!
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;
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 theTJWT
's claims using theTJWK
key with theTJOSEAlgorithmId
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:
- You have to create a
TJWK
object (the key) and aTJWS
object (the signer) - 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)
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.