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. ;-)