1

How can I generate a sha256-RSA-signed JWT token in a Karate (https://github.com/karatelabs/karate) feature file?

https://github.com/karatelabs/karate/issues/1138#issuecomment-629453412 has a nice recipee for doing such for a HMAC-SHA256 (or "HmacSHA256" in Java lingo) token, i.e. using symmetric/shared secret crypto.

But we need asymmetric crypto and the RS256 algo (see RS256 vs HS256: What's the difference? for background)...

1 Answers1

2

OK, think I figured it out :-).

Big thanks to the generous souls providing all the necessary info here:

So the following is an example Karate feature file using

  • an RS256 JWT token (put in the x-jwt header)
  • mTLS (i.e. using a client certificate for mutual TLS)

To do this one needs to make use of Karate's JavaScript and Java-interop capabilities.

This is our setup to make it work:

0 $ tree
.
├── karate-config.js
├── karate.jar
├── secrets
│   ├── client-cert-keystore.p12
│   ├── client-cert.pem
│   ├── client-cert_private-key.pem
│   ├── rsa-4096-cert.pem
│   ├── rsa-4096-private.pem
│   └── rsa-4096-public.pem
└── test.feature

1 directory, 9 files

We'll use the private key rsa-4096-private.pem (keep it secret!) of our rsa-4096-* files to create the signed token.

So the essential files for the JWT parts are

  • rsa-4096-private.pem for creating the JWT
  • rsa-4096-public.pem for verifying the token/signature, that's what the api/service/server would do with your JWT token (i.e. this file's not needed/used in our feature file). You can try verifying a resulting token with e.g. https://jwt.io/.

Sidenote: public/private key pairs can be generated with e.g. openssl.

As a bonus this example contains using a client certificate and mTLS (which httpbin probably gracefully ignores). If you don't need this you can simply strip the configure ssl... line and the client_cert_keystore_pass stuff from the karate config file and the command line.

Karate feature file:

# test.feature
Feature: Simple test

  Background: 
    # Several helper functions for creating a RS256 signed JWT token.
    # Graciously adapted from:
    # JWT generation in Karate, but with HmacSHA256:
    # https://github.com/karatelabs/karate/issues/1138#issuecomment-629453412
    # Signing with sha256 RSA signature in Java:
    # https://www.quickprogrammingtips.com/java/how-to-create-sha256-rsa-signature-using-java.html
    * def b64encode_bytes = 
      """
      function(bytes) {
          // Base64-encode `bytes`.
          // Returns bytes.
          
          var encoder = Java.type('java.util.Base64')
              .getUrlEncoder()
              .withoutPadding()
          return new java.lang.String(encoder.encode(bytes))
      }
      """

    # Base64-encode `str`, encodes str to UTF-8 and base64-encodes it.
    # Returns bytes.
    * def b64encode_str = function(str) {return b64encode_bytes(str.getBytes("UTF-8"))}
    
    * def strip_key_header_footer_ws =
      """
      function(key_text) {
          // Strip -----BEGIN ... header + footer and all newline characters.
          // Returns UTF-8-encoded bytes.

          // Need string object for replaceAll method.
          var key_text_str = new java.lang.String(key_text)
          var key_str = key_text_str
            .replaceAll("-----BEGIN PRIVATE KEY-----", "")
            .replaceAll("-----END PRIVATE KEY-----", "")
            .replaceAll("\r", "")
            .replaceAll("\n", "")
          return key_str.getBytes('UTF-8')
      }
      """

    * def sha256rsa_sign =
      """
      function(bytes, privateKey) { 
          var decoder = Java.type('java.util.Base64')
              .getDecoder()
          var PKCS8EncodedKeySpec = Java.type(
              'java.security.spec.PKCS8EncodedKeySpec')
          var spec = new PKCS8EncodedKeySpec(decoder.decode(privateKey))
          var kf = Java.type('java.security.KeyFactory').getInstance("RSA")
          var signature = Java.type('java.security.Signature')
              .getInstance("SHA256withRSA")
          signature.initSign(kf.generatePrivate(spec))
          signature.update(bytes)
          var signed = signature.sign()
          return signed
      }
      """

    * def generate_jwt_sha256rsa = 
      """
      function(payload) {
          // Generate JWT from given `payload` object (dict).
          // Returns SHA256withRSA-signed JWT token (bytes).

          var header_encoded = b64encode_str(
              JSON.stringify({alg: "RS256", typ: "JWT"}))
          var payload_encoded = b64encode_str(JSON.stringify(payload))
          var data_to_sign = header_encoded + '.' + payload_encoded
          var signature = b64encode_bytes(
              sha256rsa_sign(data_to_sign.getBytes("UTF-8"), privateKey)
              )
          var token = data_to_sign + '.' + signature
          return token
      }
      """

    # enable X509 client certificate authentication with PKCS12 file
    * configure ssl = { keyStore: 'secrets/client-cert-keystore.p12', keyStoreType: 'pkcs12', keyStorePassword: '#(client_cert_keystore_pass)' }

    # get private key for JWT generation and API key
    * def privateKeyContent = read('secrets/rsa-4096-private.pem')
    * def privateKey = strip_key_header_footer_ws(privateKeyContent)

    # generate JWT
    * def jwt = generate_jwt_sha256rsa({iss: "ExampleApp", exp: "1924902000"})

    # put all needed API access credential in the header
    * headers { x-jwt: '#(jwt)'}

    * url 'https://httpbin.org'

  Scenario Outline: get anything
    Given path '/anything/<anything_id>'
    When method get
    Then status 200

    Examples:
      | anything_id |
      | 1           |

Karate config file:

// karate-config.js
function fn() {
    //var http_proxy = java.lang.System.getenv('http_proxy');
    var client_cert_keystore_pass = java.lang.System.getenv(
        'CLIENT_CERT_KEYSTORE_PASS');

    // setup connection
    karate.configure('connectTimeout', 5000);
    karate.configure('readTimeout', 5000);
    //karate.configure('proxy', http_proxy);

    var config = {
        client_cert_keystore_pass: client_cert_keystore_pass  
    };
    return config;
}

As noted you won't need the client_cert_keystore_pass stuff unless you want mTLS. Also, you probably won't need the timeout configurations. I've tested behind a proxy so this also contains some additional config support for http_proxy (commented, left in for educational purposes). Adapt to your tastes.

Run it:

0 $ CLIENT_CERT_KEYSTORE_PASS="$PASSWORD" java -jar karate.jar -o /tmp/karate-out test.feature 
17:34:41.614 [main]  INFO  com.intuit.karate - Karate version: 1.2.1.RC1
17:34:42.076 [main]  DEBUG com.intuit.karate.Suite - [config] karate-config.js
17:34:43.942 [main]  DEBUG com.intuit.karate - key store key count for secrets/client-cert-keystore.p12: 1
17:34:44.535 [main]  DEBUG com.intuit.karate - request:
1 > GET https://httpbin.org/anything/1
1 > x-jwt: eyJhbGciO...
1 > Host: httpbin.org
1 > Connection: Keep-Alive
...


---------------------------------------------------------
feature: test.feature
scenarios:  1 | passed:  1 | failed:  0 | time: 1.7300
---------------------------------------------------------

17:34:46.577 [main]  INFO  com.intuit.karate.Suite - <<pass>> feature 1 of 1 (0 remaining) test.feature
Karate version: 1.2.1.RC1
======================================================
elapsed:   4.74 | threads:    1 | thread time: 1.73 
features:     1 | skipped:    0 | efficiency: 0.36
scenarios:    1 | passed:     1 | failed: 0
======================================================

HTML report: (paste into browser to view) | Karate version: 1.2.1.RC1
file:///tmp/karate-out/karate-reports/karate-summary.html
===================================================================

0 $ 

Note that I'm by no means a Karate expert nor a JavaScript or Java programmer. So this might well not be your idiomatic Karate/JS/Java code. ;-)