Blog

All Blog Posts  |  Next Post  |  Previous Post

Signing and Validating JWTs with Private/Public Keys in Delphi with XData

Saturday, November 16, 2024

In this post, we'll explore how to sign and validate JSON Web Tokens (JWTs) using asymmetric keys (RS256) in XData and Sphinx. 

What Are Asymmetric Keys?

Asymmetric encryption uses a pair of keys: a private key and a public key. The private key is used for signing the JWT, while the public key is used for validation. This setup ensures that the private key remains confidential and is never shared, while the public key can be distributed to anyone who needs to verify the JWT.

This differs from symmetric encryption, where the same key is used for both signing and validation. Asymmetric keys offer higher security, particularly in distributed systems where multiple parties need to verify tokens without sharing the private key.

Signing JWTs with Private Key in XData

Generating the Keys

Before diving into the code, you’ll need to generate the RSA private and public keys. You can use tools like OpenSSL or ssh-keygen to create these keys in PEM format.

For example, on Windows, you can run:

ssh-keygen -t rsa -b 2048 -m PEM -N "" -f rsa256-private.key
ssh-keygen -e -m PEM -f rsa256-private.key > rsa256-public.key

This will generate rsa256-private.key (private key) and rsa256-public.key (public key) in PEM format.

In the Code Above: Signing the JWT

In the code snippet below, the JWT is signed using the private key (rsa256-private.key) if SignWithRSA is set to True. If SignWithRSA is False, it falls back to signing the JWT with a shared secret (HS256).

const JWTSecret = 'super_secret_0123456789_0123456789'; SignWithRSA = True; RSAKeyId = 'D00CD046-FEDA-4120-9258-391371649A32'; function TLoginService.Login(const User, Password: string): string; var JWT: TJWT; JWK: TJWK; SigningKey: TArray<Byte>; SigningAlgorithm: TJOSEAlgorithmId; begin if User <> Password then raise EXDataHttpUnauthorized.Create('Invalid password'); JWT := TJWT.Create; try JWT.Claims.SetClaimOfType<string>('custom', 'data'); if SignWithRSA then begin // Load private key from file SigningKey := TFile.ReadAllBytes('rsa256-private.key'); SigningAlgorithm := TJOSEAlgorithmId.RS256; JWT.Header.KeyID := RSAKeyId; end else begin SigningKey := TEncoding.UTF8.GetBytes(JWTSecret); SigningAlgorithm := TJOSEAlgorithmId.HS256; end; JWK := TJWK.Create(SigningKey); try Result := TJOSE.SerializeCompact(JWK, SigningAlgorithm, JWT, False); finally JWK.Free; end; finally JWT.Free; end; end;

Explanation:

  • Private Key: If SignWithRSA is True, the code loads the private key (rsa256-private.key) from a file and uses it to sign the JWT using the RS256 algorithm.
  • Shared Secret: If SignWithRSA is False, the JWT is signed with a shared secret (JWTSecret) using the HS256algorithm.

This allows flexibility to choose between asymmetric and symmetric encryption based on your application's needs.

Validating JWTs with Public Key

To validate a JWT, you need to ensure it was signed by a trusted entity. In XData, this can be done in the OnGetSecretExevent, where you check the JWT's header to determine the signing algorithm and key ID.

In the Code Above: Validating the JWT

The following code validates the JWT using the public key (rsa256-public.key) if the JWT was signed using RS256. If it was signed using HS256, it uses the shared secret.

procedure TServerModule.XDataServer1JWTGetSecretEx(Sender: TObject; const JWT: TJWT; Context: THttpServerContext;
var Secret: TBytes); begin if JWT.Header.Algorithm = 'HS256' then Secret := TEncoding.UTF8.GetBytes(JWTSecret) else if JWT.Header.Algorithm = 'RS256' then begin if JWT.Header.KeyID = RSAKeyId then Secret := TFile.ReadAllBytes('rsa256-public.key') else raise EJOSEException.CreateFmt('Unknown KeyId in JWT', [JWT.Header.KeyID]); end else raise EJOSEException.CreateFmt('JWS algorithm [%s] is not supported', [JWT.Header.Algorithm]); end;

Explanation:

  • Algorithm Check: The code first checks the JWT's Algorithm field. If it's HS256, it loads the shared secret (JWTSecret). If it's RS256, it checks the KeyID and loads the corresponding public key (rsa256-public.key).
  • Public Key Validation: The public key is used to validate the JWT's signature, ensuring that the token was signed with the corresponding private key.

Configuring JWT Signing with Sphinx

Sphinx allows dynamic configuration of the JWT signing process. The TSphinxConfig.OnGetSigningData event can be used to specify whether to sign with a private key (RS256) or a shared secret (HS256).

In the Code Above: Configuring JWT Signing in Sphinx

The following code demonstrates how to configure the JWT signing using TSphinxConfig.OnGetSigningData based on an internal flag (SignWithRSA):

procedure TServerContainer.SphinxConfigGetSigningData(Sender: TObject; Args: TGetSigningDataArgs);
begin if SignWithRSA then begin Args.Data.Algorithm := 'RS256'; Args.Data.KeyId := SomeRandomKeyID; Args.Data.Key := TFile.ReadAllBytes(FileNameContainingPublicKeyInPEMFormat); end else begin Args.Data.Algorithm := 'HS256'; Args.Data.Key := TEncoding.UTF8.GetBytes(SomeRandomSharedSecretKey); end; end;

Explanation:

  • Dynamic Signing: The SphinxConfigGetSigningData event is triggered when a JWT needs to be signed. If SignWithRSA is True, it uses RS256 and loads the private key from a file. Otherwise, it uses HS256 and a shared secret.
  • Key Selection: The appropriate algorithm and key are set dynamically based on the SignWithRSA flag, offering flexibility in choosing between asymmetric and symmetric key signing.

Conclusion

Using asymmetric keys (RS256) to sign and validate JWTs provides enhanced security, ensuring that the private key is never shared. This is especially useful in distributed systems where the public key can be freely shared for validation, while the private key remains secure.

With XData and Sphinx, signing and validating JWTs with either symmetric (HS256) or asymmetric (RS256) keys is straightforward. The ability to dynamically configure the signing process based on your needs allows for a flexible and secure authentication system for your APIs.

By following these practices, you can ensure that your application remains secure and efficient, protecting your API from unauthorized access while supporting a wide range of security configurations.



Wagner Landgraf




This blog post has not received any comments yet.



Add a new comment

You will receive a confirmation mail with a link to validate your comment, please use a valid email address.
All fields are required.



All Blog Posts  |  Next Post  |  Previous Post