JWT Authentication with Delphi Series
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
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:
Token parsing: The access token (compact token) is parsed as a JWT. If the parsing fails, then the token is considered invalid.
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.
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.
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;
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;
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;
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:
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;
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
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;
JWTDemo you can find the tabsheet "JWT Consumer (Claim Validation)" that allows you to play with the
TJOSEConsumerBuilder classes and the
In this part we learned how to properly validate a JWT received (in a compact representation) through the
Stay tuned for the next part where I will explain in detail the use of custom claims in a JWT token (
TJWTClaimsClass class reference).