@platformatic/rdkafka 4.0.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +5 -5
  2. package/bench/seed.sh +0 -0
  3. package/binding.gyp +8 -1
  4. package/ci/prepublish.js +0 -1
  5. package/config.d.ts +126 -10
  6. package/configure +0 -0
  7. package/deps/librdkafka/.semaphore/run-all-tests.yml +77 -0
  8. package/deps/librdkafka/.semaphore/semaphore-integration.yml +250 -0
  9. package/deps/librdkafka/.semaphore/semaphore.yml +67 -44
  10. package/deps/librdkafka/.semaphore/verify-linux-packages.yml +41 -0
  11. package/deps/librdkafka/CHANGELOG.md +471 -1
  12. package/deps/librdkafka/CONFIGURATION.md +29 -9
  13. package/deps/librdkafka/INTRODUCTION.md +909 -601
  14. package/deps/librdkafka/LICENSES.txt +592 -1
  15. package/deps/librdkafka/Makefile +2 -1
  16. package/deps/librdkafka/_mkltmpuuWEwa.c +13 -0
  17. package/deps/librdkafka/configure +0 -0
  18. package/deps/librdkafka/debian/rules +0 -0
  19. package/deps/librdkafka/dev-conf.sh +0 -0
  20. package/deps/librdkafka/examples/consumer.c +18 -8
  21. package/deps/librdkafka/examples/describe_consumer_groups.c +25 -6
  22. package/deps/librdkafka/examples/incremental_alter_configs.c +4 -5
  23. package/deps/librdkafka/examples/kafkatest_verifiable_client.cpp +26 -42
  24. package/deps/librdkafka/examples/openssl_engine_example.cpp +3 -4
  25. package/deps/librdkafka/examples/producer.cpp +2 -2
  26. package/deps/librdkafka/examples/user_scram.c +4 -5
  27. package/deps/librdkafka/examples/win_ssl_cert_store.cpp +4 -3
  28. package/deps/librdkafka/lds-gen.py +0 -0
  29. package/deps/librdkafka/mklove/Makefile.base +0 -0
  30. package/deps/librdkafka/packaging/RELEASE.md +8 -0
  31. package/deps/librdkafka/packaging/alpine/build-alpine.sh +0 -0
  32. package/deps/librdkafka/packaging/cp/README.md +4 -1
  33. package/deps/librdkafka/packaging/cp/check_features.c +12 -4
  34. package/deps/librdkafka/packaging/cp/verify-deb.sh +6 -6
  35. package/deps/librdkafka/packaging/cp/verify-packages.sh +52 -31
  36. package/deps/librdkafka/packaging/cp/verify-rpm.sh +6 -13
  37. package/deps/librdkafka/packaging/debian/rules +0 -0
  38. package/deps/librdkafka/packaging/get_version.py +0 -0
  39. package/deps/librdkafka/packaging/homebrew/brew-update-pr.sh +0 -0
  40. package/deps/librdkafka/packaging/mingw-w64/{travis-before-install.sh → export-variables.sh} +0 -7
  41. package/deps/librdkafka/packaging/mingw-w64/semaphoreci-build.sh +1 -1
  42. package/deps/librdkafka/packaging/nuget/artifact.py +0 -0
  43. package/deps/librdkafka/packaging/nuget/cleanup-s3.py +0 -0
  44. package/deps/librdkafka/packaging/nuget/nuget.sh +0 -0
  45. package/deps/librdkafka/packaging/nuget/packaging.py +0 -0
  46. package/deps/librdkafka/packaging/nuget/push-to-nuget.sh +0 -0
  47. package/deps/librdkafka/packaging/nuget/release.py +0 -0
  48. package/deps/librdkafka/packaging/nuget/requirements.txt +3 -3
  49. package/deps/librdkafka/packaging/rpm/mock-on-docker.sh +0 -0
  50. package/deps/librdkafka/packaging/rpm/tests/run-test.sh +0 -0
  51. package/deps/librdkafka/packaging/rpm/tests/test-on-docker.sh +0 -0
  52. package/deps/librdkafka/packaging/tools/build-configurations-checks.sh +3 -1
  53. package/deps/librdkafka/packaging/tools/build-deb-package.sh +0 -0
  54. package/deps/librdkafka/packaging/tools/build-debian.sh +0 -0
  55. package/deps/librdkafka/packaging/tools/build-manylinux.sh +0 -0
  56. package/deps/librdkafka/packaging/tools/build-release-artifacts.sh +0 -0
  57. package/deps/librdkafka/packaging/tools/distro-build.sh +0 -0
  58. package/deps/librdkafka/packaging/tools/gh-release-checksums.py +0 -0
  59. package/deps/librdkafka/packaging/tools/rdutcoverage.sh +0 -0
  60. package/deps/librdkafka/packaging/tools/run-in-docker.sh +28 -0
  61. package/deps/librdkafka/packaging/tools/run-integration-tests.sh +31 -0
  62. package/deps/librdkafka/packaging/tools/run-style-check.sh +4 -0
  63. package/deps/librdkafka/packaging/tools/style-format.sh +6 -5
  64. package/deps/librdkafka/packaging/tools/update_rpcs_max_versions.py +100 -0
  65. package/deps/librdkafka/service.yml +154 -0
  66. package/deps/librdkafka/src/generate_proto.sh +0 -0
  67. package/deps/librdkafka/src/rd.h +145 -2
  68. package/deps/librdkafka/src/rdaddr.h +6 -7
  69. package/deps/librdkafka/src/rdatomic.h +15 -11
  70. package/deps/librdkafka/src/rdavg.h +3 -2
  71. package/deps/librdkafka/src/rdbase64.c +33 -2
  72. package/deps/librdkafka/src/rdbase64.h +2 -0
  73. package/deps/librdkafka/src/rdendian.h +10 -9
  74. package/deps/librdkafka/src/rdhttp.c +418 -100
  75. package/deps/librdkafka/src/rdhttp.h +23 -5
  76. package/deps/librdkafka/src/rdkafka.c +299 -145
  77. package/deps/librdkafka/src/rdkafka.h +89 -6
  78. package/deps/librdkafka/src/rdkafka_admin.c +482 -105
  79. package/deps/librdkafka/src/rdkafka_admin.h +42 -0
  80. package/deps/librdkafka/src/rdkafka_broker.c +463 -312
  81. package/deps/librdkafka/src/rdkafka_broker.h +68 -10
  82. package/deps/librdkafka/src/rdkafka_buf.c +8 -5
  83. package/deps/librdkafka/src/rdkafka_buf.h +4 -3
  84. package/deps/librdkafka/src/rdkafka_cert.c +25 -1
  85. package/deps/librdkafka/src/rdkafka_cert.h +4 -3
  86. package/deps/librdkafka/src/rdkafka_cgrp.c +468 -143
  87. package/deps/librdkafka/src/rdkafka_cgrp.h +36 -2
  88. package/deps/librdkafka/src/rdkafka_conf.c +526 -102
  89. package/deps/librdkafka/src/rdkafka_conf.h +68 -4
  90. package/deps/librdkafka/src/rdkafka_feature.c +472 -35
  91. package/deps/librdkafka/src/rdkafka_feature.h +2 -0
  92. package/deps/librdkafka/src/rdkafka_fetcher.c +22 -2
  93. package/deps/librdkafka/src/rdkafka_fetcher.h +2 -2
  94. package/deps/librdkafka/src/rdkafka_int.h +84 -46
  95. package/deps/librdkafka/src/rdkafka_metadata.c +221 -136
  96. package/deps/librdkafka/src/rdkafka_metadata.h +15 -11
  97. package/deps/librdkafka/src/rdkafka_metadata_cache.c +279 -73
  98. package/deps/librdkafka/src/rdkafka_mock.c +795 -19
  99. package/deps/librdkafka/src/rdkafka_mock.h +119 -0
  100. package/deps/librdkafka/src/rdkafka_mock_cgrp.c +1298 -132
  101. package/deps/librdkafka/src/rdkafka_mock_handlers.c +345 -49
  102. package/deps/librdkafka/src/rdkafka_mock_int.h +180 -61
  103. package/deps/librdkafka/src/rdkafka_msg.c +35 -19
  104. package/deps/librdkafka/src/rdkafka_msg.h +3 -3
  105. package/deps/librdkafka/src/rdkafka_msgset_writer.c +14 -10
  106. package/deps/librdkafka/src/rdkafka_offset.c +33 -5
  107. package/deps/librdkafka/src/rdkafka_op.c +1 -1
  108. package/deps/librdkafka/src/rdkafka_op.h +45 -21
  109. package/deps/librdkafka/src/rdkafka_partition.c +118 -23
  110. package/deps/librdkafka/src/rdkafka_partition.h +16 -7
  111. package/deps/librdkafka/src/rdkafka_proto.h +4 -1
  112. package/deps/librdkafka/src/rdkafka_protocol.h +1 -0
  113. package/deps/librdkafka/src/rdkafka_queue.c +125 -48
  114. package/deps/librdkafka/src/rdkafka_queue.h +19 -0
  115. package/deps/librdkafka/src/rdkafka_range_assignor.c +1 -1
  116. package/deps/librdkafka/src/rdkafka_request.c +336 -64
  117. package/deps/librdkafka/src/rdkafka_request.h +24 -1
  118. package/deps/librdkafka/src/rdkafka_sasl.c +5 -3
  119. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer.c +14 -10
  120. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.c +1166 -89
  121. package/deps/librdkafka/src/rdkafka_sasl_oauthbearer_oidc.h +13 -3
  122. package/deps/librdkafka/src/rdkafka_sasl_plain.c +3 -3
  123. package/deps/librdkafka/src/rdkafka_sasl_scram.c +4 -3
  124. package/deps/librdkafka/src/rdkafka_ssl.c +306 -81
  125. package/deps/librdkafka/src/rdkafka_ssl.h +22 -0
  126. package/deps/librdkafka/src/rdkafka_sticky_assignor.c +4 -0
  127. package/deps/librdkafka/src/rdkafka_telemetry.c +80 -23
  128. package/deps/librdkafka/src/rdkafka_telemetry_encode.c +5 -1
  129. package/deps/librdkafka/src/rdkafka_telemetry_encode.h +20 -20
  130. package/deps/librdkafka/src/rdkafka_topic.c +220 -137
  131. package/deps/librdkafka/src/rdkafka_topic.h +14 -8
  132. package/deps/librdkafka/src/rdkafka_transport.c +59 -47
  133. package/deps/librdkafka/src/rdkafka_transport.h +5 -0
  134. package/deps/librdkafka/src/rdkafka_txnmgr.c +6 -1
  135. package/deps/librdkafka/src/rdmap.c +6 -1
  136. package/deps/librdkafka/src/rdrand.c +11 -1
  137. package/deps/librdkafka/src/rdtime.h +48 -17
  138. package/deps/librdkafka/src/rdunittest.c +42 -25
  139. package/deps/librdkafka/src/rdunittest.h +2 -0
  140. package/deps/librdkafka/src/rdwin32.h +1 -1
  141. package/deps/librdkafka/src/regexp.c +47 -42
  142. package/deps/librdkafka/src/tinycthread_extra.c +45 -21
  143. package/deps/librdkafka/src/tinycthread_extra.h +7 -3
  144. package/deps/librdkafka/src-cpp/rdkafkacpp.h +28 -2
  145. package/deps/librdkafka/tests/0001-multiobj.c +4 -0
  146. package/deps/librdkafka/tests/0004-conf.c +103 -36
  147. package/deps/librdkafka/tests/0009-mock_cluster.c +1 -0
  148. package/deps/librdkafka/tests/0011-produce_batch.c +2 -1
  149. package/deps/librdkafka/tests/0016-client_swname.c +18 -3
  150. package/deps/librdkafka/tests/0017-compression.c +6 -8
  151. package/deps/librdkafka/tests/0018-cgrp_term.c +8 -3
  152. package/deps/librdkafka/tests/0021-rkt_destroy.c +1 -0
  153. package/deps/librdkafka/tests/0022-consume_batch.c +4 -1
  154. package/deps/librdkafka/tests/0026-consume_pause.c +12 -14
  155. package/deps/librdkafka/tests/0028-long_topicnames.c +1 -1
  156. package/deps/librdkafka/tests/0029-assign_offset.c +6 -8
  157. package/deps/librdkafka/tests/0030-offset_commit.c +2 -2
  158. package/deps/librdkafka/tests/0033-regex_subscribe.c +33 -17
  159. package/deps/librdkafka/tests/0034-offset_reset.c +25 -4
  160. package/deps/librdkafka/tests/0036-partial_fetch.c +1 -0
  161. package/deps/librdkafka/tests/0038-performance.c +1 -0
  162. package/deps/librdkafka/tests/0040-io_event.c +11 -5
  163. package/deps/librdkafka/tests/0041-fetch_max_bytes.c +1 -0
  164. package/deps/librdkafka/tests/0044-partition_cnt.c +3 -2
  165. package/deps/librdkafka/tests/0045-subscribe_update.c +284 -20
  166. package/deps/librdkafka/tests/0047-partial_buf_tmout.c +1 -0
  167. package/deps/librdkafka/tests/0048-partitioner.c +1 -1
  168. package/deps/librdkafka/tests/0050-subscribe_adds.c +1 -0
  169. package/deps/librdkafka/tests/0051-assign_adds.c +1 -0
  170. package/deps/librdkafka/tests/0052-msg_timestamps.c +22 -4
  171. package/deps/librdkafka/tests/0053-stats_cb.cpp +12 -20
  172. package/deps/librdkafka/tests/0055-producer_latency.c +176 -3
  173. package/deps/librdkafka/tests/0056-balanced_group_mt.c +6 -2
  174. package/deps/librdkafka/tests/0058-log.cpp +3 -3
  175. package/deps/librdkafka/tests/0059-bsearch.cpp +8 -4
  176. package/deps/librdkafka/tests/0061-consumer_lag.cpp +25 -5
  177. package/deps/librdkafka/tests/0067-empty_topic.cpp +3 -0
  178. package/deps/librdkafka/tests/0068-produce_timeout.c +21 -23
  179. package/deps/librdkafka/tests/0069-consumer_add_parts.c +3 -7
  180. package/deps/librdkafka/tests/0075-retry.c +38 -1
  181. package/deps/librdkafka/tests/0076-produce_retry.c +2 -0
  182. package/deps/librdkafka/tests/0077-compaction.c +8 -2
  183. package/deps/librdkafka/tests/0080-admin_ut.c +34 -4
  184. package/deps/librdkafka/tests/0081-admin.c +455 -155
  185. package/deps/librdkafka/tests/0082-fetch_max_bytes.cpp +4 -0
  186. package/deps/librdkafka/tests/0083-cb_event.c +8 -3
  187. package/deps/librdkafka/tests/0084-destroy_flags.c +14 -18
  188. package/deps/librdkafka/tests/0085-headers.cpp +4 -0
  189. package/deps/librdkafka/tests/0086-purge.c +40 -7
  190. package/deps/librdkafka/tests/0088-produce_metadata_timeout.c +1 -1
  191. package/deps/librdkafka/tests/0089-max_poll_interval.c +12 -7
  192. package/deps/librdkafka/tests/0090-idempotence.c +1 -2
  193. package/deps/librdkafka/tests/0091-max_poll_interval_timeout.c +5 -7
  194. package/deps/librdkafka/tests/0092-mixed_msgver.c +6 -0
  195. package/deps/librdkafka/tests/0093-holb.c +8 -5
  196. package/deps/librdkafka/tests/0094-idempotence_msg_timeout.c +1 -0
  197. package/deps/librdkafka/tests/0097-ssl_verify.cpp +262 -70
  198. package/deps/librdkafka/tests/0098-consumer-txn.cpp +16 -16
  199. package/deps/librdkafka/tests/0099-commit_metadata.c +1 -1
  200. package/deps/librdkafka/tests/0101-fetch-from-follower.cpp +1 -1
  201. package/deps/librdkafka/tests/0102-static_group_rebalance.c +317 -16
  202. package/deps/librdkafka/tests/0103-transactions.c +7 -7
  203. package/deps/librdkafka/tests/0104-fetch_from_follower_mock.c +1 -0
  204. package/deps/librdkafka/tests/0105-transactions_mock.c +18 -11
  205. package/deps/librdkafka/tests/0106-cgrp_sess_timeout.c +35 -14
  206. package/deps/librdkafka/tests/0107-topic_recreate.c +2 -2
  207. package/deps/librdkafka/tests/0109-auto_create_topics.cpp +86 -26
  208. package/deps/librdkafka/tests/0110-batch_size.cpp +1 -2
  209. package/deps/librdkafka/tests/0112-assign_unknown_part.c +3 -14
  210. package/deps/librdkafka/tests/0113-cooperative_rebalance.cpp +248 -104
  211. package/deps/librdkafka/tests/0114-sticky_partitioning.cpp +3 -3
  212. package/deps/librdkafka/tests/0115-producer_auth.cpp +4 -1
  213. package/deps/librdkafka/tests/0116-kafkaconsumer_close.cpp +7 -5
  214. package/deps/librdkafka/tests/0117-mock_errors.c +15 -5
  215. package/deps/librdkafka/tests/0118-commit_rebalance.c +41 -8
  216. package/deps/librdkafka/tests/0119-consumer_auth.cpp +20 -1
  217. package/deps/librdkafka/tests/0120-asymmetric_subscription.c +8 -3
  218. package/deps/librdkafka/tests/0125-immediate_flush.c +1 -1
  219. package/deps/librdkafka/tests/0126-oauthbearer_oidc.c +323 -8
  220. package/deps/librdkafka/tests/0129-fetch_aborted_msgs.c +1 -0
  221. package/deps/librdkafka/tests/0132-strategy_ordering.c +12 -4
  222. package/deps/librdkafka/tests/0133-ssl_keys.c +28 -6
  223. package/deps/librdkafka/tests/0137-barrier_batch_consume.c +24 -14
  224. package/deps/librdkafka/tests/0139-offset_validation_mock.c +510 -2
  225. package/deps/librdkafka/tests/0140-commit_metadata.cpp +1 -1
  226. package/deps/librdkafka/tests/0142-reauthentication.c +37 -17
  227. package/deps/librdkafka/tests/0143-exponential_backoff_mock.c +39 -40
  228. package/deps/librdkafka/tests/0146-metadata_mock.c +76 -15
  229. package/deps/librdkafka/tests/0147-consumer_group_consumer_mock.c +952 -0
  230. package/deps/librdkafka/tests/0148-offset_fetch_commit_error_mock.c +563 -0
  231. package/deps/librdkafka/tests/0149-broker-same-host-port.c +140 -0
  232. package/deps/librdkafka/tests/0150-telemetry_mock.c +9 -6
  233. package/deps/librdkafka/tests/0151-purge-brokers.c +566 -0
  234. package/deps/librdkafka/tests/0152-rebootstrap.c +59 -0
  235. package/deps/librdkafka/tests/0153-memberid.c +128 -0
  236. package/deps/librdkafka/tests/CMakeLists.txt +6 -0
  237. package/deps/librdkafka/tests/LibrdkafkaTestApp.py +34 -9
  238. package/deps/librdkafka/tests/autotest.sh +0 -0
  239. package/deps/librdkafka/tests/broker_version_tests.py +0 -0
  240. package/deps/librdkafka/tests/buildbox.sh +0 -0
  241. package/deps/librdkafka/tests/cleanup-checker-tests.sh +0 -0
  242. package/deps/librdkafka/tests/cluster_testing.py +0 -0
  243. package/deps/librdkafka/tests/delete-test-topics.sh +0 -0
  244. package/deps/librdkafka/tests/fixtures/oauthbearer/jwt_assertion_template.json +10 -0
  245. package/deps/librdkafka/tests/fixtures/ssl/client.keystore.intermediate.p12 +0 -0
  246. package/deps/librdkafka/tests/fixtures/ssl/client.keystore.p12 +0 -0
  247. package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.intermediate.pem +72 -0
  248. package/deps/librdkafka/tests/fixtures/ssl/client2.certificate.pem +42 -101
  249. package/deps/librdkafka/tests/fixtures/ssl/client2.intermediate.key +46 -0
  250. package/deps/librdkafka/tests/fixtures/ssl/client2.key +41 -29
  251. package/deps/librdkafka/tests/fixtures/ssl/create_keys.sh +112 -37
  252. package/deps/librdkafka/tests/gen-ssl-certs.sh +0 -0
  253. package/deps/librdkafka/tests/interactive_broker_version.py +0 -0
  254. package/deps/librdkafka/tests/java/Makefile +2 -1
  255. package/deps/librdkafka/tests/java/run-class.sh +0 -0
  256. package/deps/librdkafka/tests/lz4_manual_test.sh +0 -0
  257. package/deps/librdkafka/tests/multi-broker-version-test.sh +0 -0
  258. package/deps/librdkafka/tests/parse-refcnt.sh +0 -0
  259. package/deps/librdkafka/tests/performance_plot.py +0 -0
  260. package/deps/librdkafka/tests/requirements.txt +1 -1
  261. package/deps/librdkafka/tests/run-all-tests.sh +79 -0
  262. package/deps/librdkafka/tests/run-consumer-tests.sh +0 -0
  263. package/deps/librdkafka/tests/run-producer-tests.sh +0 -0
  264. package/deps/librdkafka/tests/run-test-batches.py +157 -0
  265. package/deps/librdkafka/tests/run-test.sh +0 -0
  266. package/deps/librdkafka/tests/sasl_test.py +0 -0
  267. package/deps/librdkafka/tests/test.c +478 -18
  268. package/deps/librdkafka/tests/test.h +62 -13
  269. package/deps/librdkafka/tests/testcpp.cpp +6 -1
  270. package/deps/librdkafka/tests/testcpp.h +30 -2
  271. package/deps/librdkafka/tests/testshared.h +16 -2
  272. package/deps/librdkafka/tests/tools/stats/graph.py +0 -0
  273. package/deps/librdkafka/tests/tools/stats/to_csv.py +1 -1
  274. package/deps/librdkafka/tests/trivup/trivup-0.14.0.tar.gz +0 -0
  275. package/deps/librdkafka/tests/until-fail.sh +0 -0
  276. package/deps/librdkafka/tests/xxxx-metadata.cpp +1 -1
  277. package/deps/librdkafka/vcpkg.json +1 -1
  278. package/deps/librdkafka/win32/setup-msys2.ps1 +24 -8
  279. package/deps/librdkafka/win32/setup-vcpkg.ps1 +28 -6
  280. package/deps/librdkafka/win32/tests/tests.vcxproj +6 -0
  281. package/deps/librdkafka/win32/wingetopt.h +5 -4
  282. package/errors.d.ts +27 -1
  283. package/lib/error.js +29 -3
  284. package/make_docs.sh +0 -0
  285. package/package.json +11 -11
  286. package/run_docker.sh +0 -0
  287. package/src/callbacks.cc +6 -9
  288. package/src/callbacks.h +1 -5
  289. package/test/kafka-consumer-worker.js +10 -7
  290. package/test/kafka-consumer.spec.js +32 -21
  291. package/ci/update-version.js +0 -123
  292. package/deps/librdkafka/.semaphore/project.yml +0 -43
  293. package/deps/librdkafka/.semaphore/project_public.yml +0 -20
  294. package/deps/librdkafka/win32/install-openssl.ps1 +0 -33
@@ -51,8 +51,9 @@
51
51
  *
52
52
  * @locality Any thread.
53
53
  */
54
- static char *rd_kafka_oidc_build_auth_header(const char *client_id,
55
- const char *client_secret) {
54
+ static char *
55
+ rd_kafka_oidc_client_credentials_build_auth_header(const char *client_id,
56
+ const char *client_secret) {
56
57
 
57
58
  rd_chariov_t client_authorization_in;
58
59
  rd_chariov_t client_authorization_out;
@@ -90,13 +91,15 @@ static char *rd_kafka_oidc_build_auth_header(const char *client_id,
90
91
  *
91
92
  * @locality Any thread.
92
93
  */
93
- static void rd_kafka_oidc_build_headers(const char *client_id,
94
- const char *client_secret,
95
- struct curl_slist **headersp) {
94
+ static void
95
+ rd_kafka_oidc_client_credentials_build_headers(const char *client_id,
96
+ const char *client_secret,
97
+ struct curl_slist **headersp) {
96
98
  char *authorization_base64_header;
97
99
 
98
100
  authorization_base64_header =
99
- rd_kafka_oidc_build_auth_header(client_id, client_secret);
101
+ rd_kafka_oidc_client_credentials_build_auth_header(client_id,
102
+ client_secret);
100
103
 
101
104
  *headersp = curl_slist_append(*headersp, "Accept: application/json");
102
105
  *headersp = curl_slist_append(*headersp, authorization_base64_header);
@@ -198,9 +201,10 @@ done:
198
201
  * The post_fields_size will be returned in \p post_fields_size.
199
202
  *
200
203
  */
201
- static void rd_kafka_oidc_build_post_fields(const char *scope,
202
- char **post_fields,
203
- size_t *post_fields_size) {
204
+ static void
205
+ rd_kafka_oidc_client_credentials_build_post_fields(const char *scope,
206
+ char **post_fields,
207
+ size_t *post_fields_size) {
204
208
  size_t scope_size = 0;
205
209
 
206
210
  if (scope)
@@ -217,6 +221,684 @@ static void rd_kafka_oidc_build_post_fields(const char *scope,
217
221
  }
218
222
  }
219
223
 
224
+ /**
225
+ * @brief Get JWT algorithm label string for the specified signing algorithm.
226
+ *
227
+ * @param token_signing_algo The algorithm enum value
228
+ *
229
+ * @returns String representation of the algorithm.
230
+ *
231
+ * @locality Any thread.
232
+ */
233
+ static char *rd_kafka_oidc_assertion_get_algo_label(
234
+ const rd_kafka_oauthbearer_assertion_algorithm_t token_signing_algo) {
235
+ switch (token_signing_algo) {
236
+ case RD_KAFKA_SASL_OAUTHBEARER_ASSERTION_ALGORITHM_RS256:
237
+ return "RS256";
238
+ case RD_KAFKA_SASL_OAUTHBEARER_ASSERTION_ALGORITHM_ES256:
239
+ return "ES256";
240
+ default:
241
+ rd_assert(!*"Unknown JOSE algorithm");
242
+ return NULL;
243
+ }
244
+ }
245
+
246
+ /**
247
+ * @brief Parse a JWT template file and extract header and payload JSON
248
+ * objects.
249
+ *
250
+ * Reads and parses the JWT template file, which should contain a JSON object
251
+ * with "header" and "payload" properties.
252
+ *
253
+ * @param rk
254
+ * @param jwt_template_file_path Path to the template file
255
+ * @param header Pointer to store the parsed header JSON object
256
+ * @param payload Pointer to store the parsed payload JSON object
257
+ *
258
+ * @returns 0 on success, -1 on failure
259
+ *
260
+ * @locality Any thread.
261
+ */
262
+ static int
263
+ rd_kafka_oidc_assertion_parse_template_file(rd_kafka_t *rk,
264
+ const char *jwt_template_file_path,
265
+ cJSON **header,
266
+ cJSON **payload) {
267
+ char *template_content = NULL;
268
+ cJSON *template_json = NULL;
269
+ int ret = -1;
270
+ size_t file_size;
271
+
272
+ *header = NULL;
273
+ *payload = NULL;
274
+
275
+ template_content =
276
+ rd_file_read(jwt_template_file_path, &file_size, 1024 * 1024);
277
+ if (!template_content) {
278
+ rd_kafka_log(rk, LOG_ERR, "JWT",
279
+ "Failed to open JWT template file: %s",
280
+ jwt_template_file_path);
281
+ return -1;
282
+ }
283
+
284
+ if (file_size == 0) {
285
+ rd_kafka_log(rk, LOG_ERR, "JWT",
286
+ "JWT template file is empty or invalid");
287
+ rd_free(template_content);
288
+ return -1;
289
+ }
290
+
291
+ template_json = cJSON_Parse((char *)template_content);
292
+ if (!template_json) {
293
+ rd_kafka_log(rk, LOG_ERR, "JWT",
294
+ "Failed to parse JWT template JSON");
295
+ goto cleanup;
296
+ }
297
+
298
+ cJSON *header_item = cJSON_GetObjectItem(template_json, "header");
299
+ cJSON *payload_item = cJSON_GetObjectItem(template_json, "payload");
300
+
301
+ if (!header_item || !payload_item) {
302
+ rd_kafka_log(rk, LOG_ERR, "JWT",
303
+ "JWT template must contain both 'header' "
304
+ "and 'payload' objects");
305
+ goto cleanup;
306
+ }
307
+
308
+ *header = cJSON_Duplicate(header_item, 1);
309
+ *payload = cJSON_Duplicate(payload_item, 1);
310
+
311
+ if (!*header || !*payload) {
312
+ rd_kafka_log(rk, LOG_ERR, "JWT",
313
+ "Failed to duplicate header or payload objects");
314
+ if (*header) {
315
+ cJSON_Delete(*header);
316
+ *header = NULL;
317
+ }
318
+ goto cleanup;
319
+ }
320
+
321
+ ret = 0;
322
+
323
+ cleanup:
324
+ if (template_content)
325
+ rd_free(template_content);
326
+ if (template_json)
327
+ cJSON_Delete(template_json);
328
+
329
+ return ret;
330
+ }
331
+
332
+ /**
333
+ * @brief Create JWT assertion.
334
+ *
335
+ * Creates a JWT token signed with the specified private key using the
336
+ * algorithm specified. The token can be created from a template file or
337
+ * will create a minimal default token if no template is provided.
338
+ *
339
+ * @param rk The rd_kafka_t instance for logging
340
+ * @param private_key_pem PEM formatted private key string (mutually exclusive
341
+ * with key_file_location)
342
+ * @param key_file_location Path to private key file (mutually exclusive with
343
+ * private_key_pem)
344
+ * @param passphrase Optional passphrase for encrypted private key
345
+ * @param token_signing_algo Algorithm to use for signing (RS256 or ES256)
346
+ * @param jwt_template_file Optional path to JWT template file
347
+ * @param subject Optional subject claim value.
348
+ * @param issuer Optional issuer claim value.
349
+ * @param audience Optional audience claim value.
350
+ * @param nbf `nbf` claim value to express seconds of validity in the past.
351
+ * @param exp `exp` claim value to express seconds of validity in the future.
352
+ * @param jti_include Whether to include a JTI claim (UUID)
353
+ *
354
+ * @returns Newly allocated JWT string, caller must free with rd_free(). NULL on
355
+ * error.
356
+ *
357
+ * @locality Any thread.
358
+ */
359
+ static char *rd_kafka_oidc_assertion_create(
360
+ rd_kafka_t *rk,
361
+ const char *private_key_pem,
362
+ const char *key_file_location,
363
+ const char *passphrase,
364
+ const rd_kafka_oauthbearer_assertion_algorithm_t token_signing_algo,
365
+ const char *jwt_template_file,
366
+ const char *subject,
367
+ const char *issuer,
368
+ const char *audience,
369
+ const int nbf,
370
+ const int exp,
371
+ const rd_bool_t jti_include) {
372
+
373
+ char *encoded_header = NULL;
374
+ char *encoded_payload = NULL;
375
+ char *encoded_signature = NULL;
376
+ char *unsigned_token = NULL;
377
+ char *result = NULL;
378
+ char *header_str = NULL;
379
+ char *payload_str = NULL;
380
+ EVP_PKEY *pkey = NULL;
381
+ BIO *bio = NULL;
382
+ cJSON *header_json_obj = NULL;
383
+ cJSON *payload_json_obj = NULL;
384
+ EVP_MD_CTX *mdctx = NULL;
385
+ unsigned char *sig = NULL;
386
+ rd_chariov_t header_iov;
387
+ rd_chariov_t payload_iov;
388
+ rd_chariov_t sig_iov;
389
+ rd_kafka_Uuid_t jti_uuid;
390
+ char *jti_uuid_str = NULL;
391
+
392
+ rd_ts_t issued_at = rd_uclock() / 1000000;
393
+ rd_ts_t not_before = issued_at - nbf;
394
+ rd_ts_t expiration_time = issued_at + exp;
395
+
396
+ if (jwt_template_file) {
397
+ if (rd_kafka_oidc_assertion_parse_template_file(
398
+ rk, jwt_template_file, &header_json_obj,
399
+ &payload_json_obj) != 0) {
400
+ rd_kafka_log(rk, LOG_ERR, "JWT",
401
+ "Failed to process JWT template file %s",
402
+ jwt_template_file);
403
+ return NULL;
404
+ }
405
+ } else {
406
+ header_json_obj = cJSON_CreateObject();
407
+ payload_json_obj = cJSON_CreateObject();
408
+ }
409
+
410
+ /* Add required header fields */
411
+ cJSON_DeleteItemFromObjectCaseSensitive(header_json_obj, "alg");
412
+ cJSON_DeleteItemFromObjectCaseSensitive(header_json_obj, "typ");
413
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj, "iat");
414
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj, "exp");
415
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj, "nbf");
416
+ cJSON_AddStringToObject(
417
+ header_json_obj, "alg",
418
+ rd_kafka_oidc_assertion_get_algo_label(token_signing_algo));
419
+ cJSON_AddStringToObject(header_json_obj, "typ", "JWT");
420
+
421
+ /* Add required payload fields */
422
+ cJSON_AddNumberToObject(payload_json_obj, "iat", (double)issued_at);
423
+ cJSON_AddNumberToObject(payload_json_obj, "exp",
424
+ (double)expiration_time);
425
+ cJSON_AddNumberToObject(payload_json_obj, "nbf", (double)not_before);
426
+
427
+ if (subject) {
428
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj,
429
+ "sub");
430
+ cJSON_AddStringToObject(payload_json_obj, "sub", subject);
431
+ }
432
+
433
+ if (issuer) {
434
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj,
435
+ "iss");
436
+ cJSON_AddStringToObject(payload_json_obj, "iss", issuer);
437
+ }
438
+
439
+ if (audience) {
440
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj,
441
+ "aud");
442
+ cJSON_AddStringToObject(payload_json_obj, "aud", audience);
443
+ }
444
+
445
+ if (jti_include) {
446
+ jti_uuid = rd_kafka_Uuid_random();
447
+ jti_uuid_str = rd_kafka_Uuid_str(&jti_uuid);
448
+ cJSON_DeleteItemFromObjectCaseSensitive(payload_json_obj,
449
+ "jti");
450
+ cJSON_AddStringToObject(payload_json_obj, "jti", jti_uuid_str);
451
+ rd_free(jti_uuid_str);
452
+ }
453
+
454
+ header_str = cJSON_PrintUnformatted(header_json_obj);
455
+ payload_str = cJSON_PrintUnformatted(payload_json_obj);
456
+
457
+ if (!header_str || !payload_str) {
458
+ rd_kafka_log(rk, LOG_ERR, "JWT",
459
+ "Failed to convert template objects to JSON");
460
+ goto cleanup;
461
+ }
462
+
463
+ header_iov.ptr = header_str;
464
+ header_iov.size = strlen(header_str);
465
+ encoded_header = rd_base64_encode_str_urlsafe(&header_iov);
466
+
467
+ payload_iov.ptr = payload_str;
468
+ payload_iov.size = strlen(payload_str);
469
+ encoded_payload = rd_base64_encode_str_urlsafe(&payload_iov);
470
+ if (!encoded_header || !encoded_payload)
471
+ goto cleanup;
472
+
473
+ size_t unsigned_token_len =
474
+ strlen(encoded_header) + strlen(encoded_payload) + 2;
475
+ unsigned_token = rd_malloc(unsigned_token_len);
476
+
477
+ if (!unsigned_token)
478
+ goto cleanup;
479
+ rd_snprintf(unsigned_token, unsigned_token_len, "%s.%s", encoded_header,
480
+ encoded_payload);
481
+
482
+ if (private_key_pem) {
483
+ bio = BIO_new_mem_buf((void *)private_key_pem, -1);
484
+ } else if (key_file_location) {
485
+ bio = BIO_new_file(key_file_location, "r");
486
+ }
487
+
488
+ if (!bio) {
489
+ rd_kafka_log(rk, LOG_ERR, "JWT",
490
+ "Failed to create BIO for private key");
491
+ goto cleanup;
492
+ }
493
+
494
+ if (passphrase) {
495
+ pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL,
496
+ (void *)passphrase);
497
+ } else {
498
+ pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
499
+ }
500
+ BIO_free(bio);
501
+ bio = NULL;
502
+
503
+ if (!pkey) {
504
+ rd_kafka_log(rk, LOG_ERR, "JWT", "Failed to load private key");
505
+ goto cleanup;
506
+ }
507
+
508
+ mdctx = EVP_MD_CTX_new();
509
+ if (!mdctx) {
510
+ rd_kafka_log(rk, LOG_ERR, "JWT",
511
+ "Failed to create message digest context");
512
+ goto cleanup;
513
+ }
514
+
515
+ const EVP_MD *md = EVP_sha256(); /* Both RS256 and ES256 use SHA-256 */
516
+
517
+ if (EVP_DigestSignInit(mdctx, NULL, md, NULL, pkey) != 1) {
518
+ rd_kafka_log(rk, LOG_ERR, "JWT",
519
+ "Failed to initialize signing context");
520
+ goto cleanup;
521
+ }
522
+
523
+ if (EVP_DigestSignUpdate(mdctx, unsigned_token,
524
+ strlen(unsigned_token)) != 1) {
525
+ rd_kafka_log(rk, LOG_ERR, "JWT",
526
+ "Failed to update digest with token data");
527
+ goto cleanup;
528
+ }
529
+
530
+ size_t siglen = 0;
531
+ if (EVP_DigestSignFinal(mdctx, NULL, &siglen) != 1) {
532
+ rd_kafka_log(rk, LOG_ERR, "JWT",
533
+ "Failed to get signature length");
534
+ goto cleanup;
535
+ }
536
+
537
+ sig = rd_malloc(siglen);
538
+ if (!sig) {
539
+ rd_kafka_log(rk, LOG_ERR, "JWT",
540
+ "Failed to allocate memory for signature");
541
+ goto cleanup;
542
+ }
543
+
544
+ if (EVP_DigestSignFinal(mdctx, sig, &siglen) != 1) {
545
+ rd_kafka_log(rk, LOG_ERR, "JWT", "Failed to create signature");
546
+ goto cleanup;
547
+ }
548
+
549
+ sig_iov.ptr = (char *)sig;
550
+ sig_iov.size = siglen;
551
+ encoded_signature = rd_base64_encode_str_urlsafe(&sig_iov);
552
+
553
+ if (!encoded_signature)
554
+ goto cleanup;
555
+
556
+ size_t jwt_len = strlen(encoded_header) + strlen(encoded_payload) +
557
+ strlen(encoded_signature) + 3;
558
+ result = rd_malloc(jwt_len);
559
+ if (!result)
560
+ goto cleanup;
561
+ rd_snprintf(result, jwt_len, "%s.%s.%s", encoded_header,
562
+ encoded_payload, encoded_signature);
563
+
564
+ cleanup:
565
+ if (encoded_header)
566
+ rd_free(encoded_header);
567
+ if (encoded_payload)
568
+ rd_free(encoded_payload);
569
+ if (encoded_signature)
570
+ rd_free(encoded_signature);
571
+ if (unsigned_token)
572
+ rd_free(unsigned_token);
573
+ if (sig)
574
+ rd_free(sig);
575
+
576
+ if (header_json_obj) {
577
+ if (header_str)
578
+ free(header_str); /* cJSON_PrintUnformatted uses malloc
579
+ */
580
+ cJSON_Delete(header_json_obj);
581
+ } else if (header_str) {
582
+ rd_free(header_str); /* rd_malloc was used */
583
+ }
584
+
585
+ if (payload_json_obj) {
586
+ if (payload_str)
587
+ free(payload_str); /* cJSON_PrintUnformatted uses malloc
588
+ */
589
+ cJSON_Delete(payload_json_obj);
590
+ } else if (payload_str) {
591
+ rd_free(payload_str); /* rd_malloc was used */
592
+ }
593
+
594
+ if (pkey)
595
+ EVP_PKEY_free(pkey);
596
+ if (mdctx)
597
+ EVP_MD_CTX_free(mdctx);
598
+
599
+ return result;
600
+ }
601
+
602
+
603
+ /**
604
+ * @brief Build request body for JWT bearer token request.
605
+ *
606
+ * Creates a URL-encoded request body for token exchange with the JWT assertion
607
+ * and optional scope.
608
+ *
609
+ * @param assertion The JWT assertion to include in the request.
610
+ * @param scope Optional scope to include in the request (will be URL encoded).
611
+ *
612
+ * @returns Newly allocated string with the URL-encoded request body.
613
+ * Caller must free with rd_free(). NULL on memory allocation failure.
614
+ *
615
+ * @locality Any thread.
616
+ */
617
+ static char *rd_kafka_oidc_jwt_bearer_build_request_body(const char *assertion,
618
+ const char *scope) {
619
+ const char *assertion_prefix =
620
+ "grant_type=urn:ietf:params:oauth:"
621
+ "grant-type:jwt-bearer"
622
+ "&assertion=";
623
+ int assertion_prefix_len = strlen(assertion_prefix) + strlen(assertion);
624
+ int body_size = assertion_prefix_len + 1;
625
+ char *scope_escaped = NULL;
626
+ if (scope) {
627
+ scope_escaped = curl_easy_escape(NULL, scope, 0);
628
+ body_size += strlen("&scope=") + strlen(scope_escaped);
629
+ }
630
+
631
+ char *body = rd_malloc(body_size);
632
+
633
+ rd_snprintf(body, body_size, "%s%s", assertion_prefix, assertion);
634
+ if (scope) {
635
+ rd_snprintf(&body[assertion_prefix_len],
636
+ body_size - assertion_prefix_len, "&scope=%s",
637
+ scope_escaped);
638
+ rd_free(scope_escaped);
639
+ }
640
+ return body;
641
+ }
642
+
643
+ /**
644
+ * @brief JWT assertion from file
645
+ *
646
+ * @param file_path Path to the file containing the JWT assertion.
647
+ *
648
+ * @returns Newly allocated string with the JWT assertion.
649
+ * Caller must free with rd_free(). NULL on error.
650
+ */
651
+ static char *rd_kafka_oidc_assertion_read_from_file(const char *file_path) {
652
+ if (!file_path)
653
+ return NULL;
654
+ const size_t max_size = 1024 * 1024; /* 1MB limit */
655
+ return rd_file_read(file_path, NULL, max_size);
656
+ }
657
+
658
+ /**
659
+ * @brief Try to validate a token field from the JSON response.
660
+ * Extracts and validates the token, then decodes its payload to get
661
+ * subject and expiration.
662
+ *
663
+ * @param rk The rd_kafka_t instance
664
+ * @param json The JSON response from the token endpoint
665
+ * @param field_name The name of the field to extract (e.g., "access_token" or
666
+ * "id_token")
667
+ * @param token_out Pointer to store the extracted token
668
+ * @param sub_out Pointer to store the subject from the token
669
+ * @param exp_out Pointer to store the expiration from the token
670
+ * @param errstr_out Buffer to store error message
671
+ * @param errstr_size Size of error message buffer
672
+ *
673
+ * @returns The extracted token or NULL on failure.
674
+ */
675
+ static char *rd_kafka_oidc_token_try_validate(cJSON *json,
676
+ const char *field,
677
+ char **sub,
678
+ double *exp,
679
+ char *errstr,
680
+ size_t errstr_size) {
681
+ cJSON *access_token_json, *jwt_exp, *jwt_sub, *payloads = NULL;
682
+ char *jwt_token = NULL, *decoded_payloads = NULL;
683
+ const char *decode_errstr = NULL;
684
+ *sub = NULL;
685
+
686
+ access_token_json = cJSON_GetObjectItem(json, field);
687
+
688
+ if (!access_token_json) {
689
+ rd_snprintf(errstr, errstr_size,
690
+ "Expected JSON response with \"%s\" field", field);
691
+ goto fail;
692
+ }
693
+
694
+ jwt_token = cJSON_GetStringValue(access_token_json);
695
+ if (!jwt_token) {
696
+ rd_snprintf(errstr, errstr_size,
697
+ "Expected token as a string value");
698
+ goto fail;
699
+ }
700
+
701
+ decode_errstr =
702
+ rd_kafka_jwt_b64_decode_payload(jwt_token, &decoded_payloads);
703
+ if (decode_errstr != NULL) {
704
+ rd_snprintf(errstr, errstr_size,
705
+ "Failed to decode JWT payload: %s", decode_errstr);
706
+ goto fail;
707
+ }
708
+
709
+ payloads = cJSON_Parse(decoded_payloads);
710
+ if (payloads == NULL) {
711
+ rd_snprintf(errstr, errstr_size,
712
+ "Failed to parse JSON JWT payload");
713
+ goto fail;
714
+ }
715
+
716
+ jwt_exp = cJSON_GetObjectItem(payloads, "exp");
717
+ if (jwt_exp == NULL) {
718
+ rd_snprintf(errstr, errstr_size,
719
+ "Expected JSON JWT response with "
720
+ "\"exp\" field");
721
+ goto fail;
722
+ }
723
+
724
+ *exp = cJSON_GetNumberValue(jwt_exp);
725
+ if (*exp <= 0) {
726
+ rd_snprintf(errstr, errstr_size,
727
+ "Expected JSON JWT response with "
728
+ "valid \"exp\" field");
729
+ goto fail;
730
+ }
731
+
732
+ jwt_sub = cJSON_GetObjectItem(payloads, "sub");
733
+ if (jwt_sub == NULL) {
734
+ rd_snprintf(errstr, errstr_size,
735
+ "Expected JSON JWT response with "
736
+ "\"sub\" field");
737
+ goto fail;
738
+ }
739
+
740
+ *sub = cJSON_GetStringValue(jwt_sub);
741
+ if (*sub == NULL) {
742
+ rd_snprintf(errstr, errstr_size,
743
+ "Expected JSON JWT response with "
744
+ "valid \"sub\" field");
745
+ goto fail;
746
+ }
747
+ *sub = rd_strdup(*sub);
748
+ done:
749
+ if (payloads)
750
+ cJSON_Delete(payloads);
751
+ if (decoded_payloads)
752
+ rd_free(decoded_payloads);
753
+ return jwt_token;
754
+ fail:
755
+ jwt_token = NULL;
756
+ goto done;
757
+ }
758
+
759
+ /**
760
+ * @brief Implementation of JWT token refresh callback function.
761
+ * Creates a JWT assertion, exchanges it for an access token,
762
+ * and sets the token for SASL OAUTHBEARER authentication.
763
+ *
764
+ * @param rk The rd_kafka_t instance.
765
+ * @param oauthbearer_config The OAUTHBEARER configuration.
766
+ * @param opaque Opaque pointer passed to the callback.
767
+ *
768
+ * @locality rdkafka main thread
769
+ */
770
+ void rd_kafka_oidc_token_jwt_bearer_refresh_cb(rd_kafka_t *rk,
771
+ const char *oauthbearer_config,
772
+ void *opaque) {
773
+ const int timeout_s = 20;
774
+ const int retry = 4;
775
+ const int retry_ms = 5 * 1000;
776
+
777
+ char *jwt_assertion = NULL;
778
+ char *request_body = NULL;
779
+ struct curl_slist *headers = NULL;
780
+ rd_http_error_t *herr = NULL;
781
+ cJSON *json = NULL;
782
+ char *jwt_token = NULL;
783
+ char set_token_errstr[512];
784
+ double exp = 0;
785
+ char **extensions = NULL;
786
+ char **extension_key_value = NULL;
787
+ size_t extension_key_value_cnt = 0;
788
+ size_t extension_cnt;
789
+ char *sub = NULL;
790
+ char validate_errstr[512];
791
+
792
+ if (rd_kafka_terminating(rk))
793
+ return;
794
+
795
+ if (rk->rk_conf.sasl.oauthbearer.assertion.file) {
796
+ jwt_assertion = rd_kafka_oidc_assertion_read_from_file(
797
+ rk->rk_conf.sasl.oauthbearer.assertion.file);
798
+ } else {
799
+ jwt_assertion = rd_kafka_oidc_assertion_create(
800
+ rk, rk->rk_conf.sasl.oauthbearer.assertion.private_key.pem,
801
+ rk->rk_conf.sasl.oauthbearer.assertion.private_key.file,
802
+ rk->rk_conf.sasl.oauthbearer.assertion.private_key
803
+ .passphrase,
804
+ rk->rk_conf.sasl.oauthbearer.assertion.algorithm,
805
+ rk->rk_conf.sasl.oauthbearer.assertion.jwt_template_file,
806
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.subject,
807
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.issuer,
808
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.audience,
809
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.not_before_s,
810
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.expiration_s,
811
+ rk->rk_conf.sasl.oauthbearer.assertion.claim.jti_include);
812
+ }
813
+
814
+ if (!jwt_assertion) {
815
+ rd_kafka_oauthbearer_set_token_failure(
816
+ rk, "Failed to create JWT assertion");
817
+ goto done;
818
+ }
819
+
820
+ request_body = rd_kafka_oidc_jwt_bearer_build_request_body(
821
+ jwt_assertion, rk->rk_conf.sasl.oauthbearer.scope);
822
+
823
+ if (!request_body) {
824
+ rd_kafka_oauthbearer_set_token_failure(
825
+ rk, "Failed to build JWT request body");
826
+ goto done;
827
+ }
828
+
829
+ headers = curl_slist_append(
830
+ headers, "Content-Type: application/x-www-form-urlencoded");
831
+ headers = curl_slist_append(headers, "Accept: application/json");
832
+
833
+ herr = rd_http_post_expect_json(
834
+ rk, rk->rk_conf.sasl.oauthbearer.token_endpoint_url, headers,
835
+ request_body, strlen(request_body), timeout_s, retry, retry_ms,
836
+ &json);
837
+
838
+ if (unlikely(herr != NULL)) {
839
+ rd_kafka_log(
840
+ rk, LOG_ERR, "JWT",
841
+ "Failed to retrieve JWT token from \"%s\": %s (%d)",
842
+ rk->rk_conf.sasl.oauthbearer.token_endpoint_url,
843
+ herr->errstr, herr->code);
844
+ rd_kafka_oauthbearer_set_token_failure(rk, herr->errstr);
845
+ rd_http_error_destroy(herr);
846
+ goto done;
847
+ }
848
+
849
+ /*
850
+ * RFC 7523 Section 1 says that an access token should be returned
851
+ * https://datatracker.ietf.org/doc/html/rfc7523#section-1
852
+ * Some providers (e.g. GCP) return an `id_token` instead, depending
853
+ * on the configured `target_audience` in the request JWT bearer token.
854
+ * This may be because the validation endpoint is not accessible
855
+ * for validating the `access_token` while the `id_token` is validated
856
+ * through the JWKS URL.
857
+ * This function will try to validate the `access_token` and then the
858
+ * `id_token`.
859
+ */
860
+ jwt_token = rd_kafka_oidc_token_try_validate(json, "access_token", &sub,
861
+ &exp, validate_errstr,
862
+ sizeof(validate_errstr));
863
+ if (!jwt_token)
864
+ jwt_token = rd_kafka_oidc_token_try_validate(
865
+ json, "id_token", &sub, &exp, validate_errstr,
866
+ sizeof(validate_errstr));
867
+
868
+ if (!jwt_token) {
869
+ rd_kafka_oauthbearer_set_token_failure(rk, validate_errstr);
870
+ goto done;
871
+ }
872
+
873
+
874
+ if (rk->rk_conf.sasl.oauthbearer.extensions_str) {
875
+ extensions =
876
+ rd_string_split(rk->rk_conf.sasl.oauthbearer.extensions_str,
877
+ ',', rd_true, &extension_cnt);
878
+
879
+ extension_key_value = rd_kafka_conf_kv_split(
880
+ (const char **)extensions, extension_cnt,
881
+ &extension_key_value_cnt);
882
+ }
883
+
884
+ if (rd_kafka_oauthbearer_set_token(
885
+ rk, jwt_token, (int64_t)exp * 1000, sub,
886
+ (const char **)extension_key_value, extension_key_value_cnt,
887
+ set_token_errstr,
888
+ sizeof(set_token_errstr)) != RD_KAFKA_RESP_ERR_NO_ERROR) {
889
+ rd_kafka_oauthbearer_set_token_failure(rk, validate_errstr);
890
+ }
891
+
892
+ done:
893
+ RD_IF_FREE(sub, rd_free);
894
+ RD_IF_FREE(jwt_assertion, rd_free);
895
+ RD_IF_FREE(request_body, rd_free);
896
+ RD_IF_FREE(headers, curl_slist_free_all);
897
+ RD_IF_FREE(json, cJSON_Delete);
898
+ RD_IF_FREE(extensions, rd_free);
899
+ RD_IF_FREE(extension_key_value, rd_free);
900
+ /* jwt_token is freed as part of the json object */
901
+ }
220
902
 
221
903
  /**
222
904
  * @brief Implementation of Oauth/OIDC token refresh callback function,
@@ -224,37 +906,33 @@ static void rd_kafka_oidc_build_post_fields(const char *scope,
224
906
  * then extract the jwt from the JSON response, and forward it to
225
907
  * the broker.
226
908
  */
227
- void rd_kafka_oidc_token_refresh_cb(rd_kafka_t *rk,
228
- const char *oauthbearer_config,
229
- void *opaque) {
909
+ void rd_kafka_oidc_token_client_credentials_refresh_cb(
910
+ rd_kafka_t *rk,
911
+ const char *oauthbearer_config,
912
+ void *opaque) {
230
913
  const int timeout_s = 20;
231
914
  const int retry = 4;
232
915
  const int retry_ms = 5 * 1000;
233
916
 
234
917
  double exp;
235
918
 
236
- cJSON *json = NULL;
237
- cJSON *payloads = NULL;
238
- cJSON *parsed_token, *jwt_exp, *jwt_sub;
919
+ cJSON *json = NULL;
239
920
 
240
921
  rd_http_error_t *herr;
241
922
 
242
923
  char *jwt_token;
243
- char *post_fields;
244
- char *decoded_payloads = NULL;
924
+ char *post_fields = NULL;
245
925
 
246
926
  struct curl_slist *headers = NULL;
247
927
 
248
928
  const char *token_url;
249
- const char *sub;
250
- const char *errstr;
929
+ char *sub = NULL;
251
930
 
252
931
  size_t post_fields_size;
253
932
  size_t extension_cnt;
254
933
  size_t extension_key_value_cnt = 0;
255
934
 
256
935
  char set_token_errstr[512];
257
- char decode_payload_errstr[512];
258
936
 
259
937
  char **extensions = NULL;
260
938
  char **extension_key_value = NULL;
@@ -262,13 +940,14 @@ void rd_kafka_oidc_token_refresh_cb(rd_kafka_t *rk,
262
940
  if (rd_kafka_terminating(rk))
263
941
  return;
264
942
 
265
- rd_kafka_oidc_build_headers(rk->rk_conf.sasl.oauthbearer.client_id,
266
- rk->rk_conf.sasl.oauthbearer.client_secret,
267
- &headers);
943
+ rd_kafka_oidc_client_credentials_build_headers(
944
+ rk->rk_conf.sasl.oauthbearer.client_id,
945
+ rk->rk_conf.sasl.oauthbearer.client_secret, &headers);
268
946
 
269
947
  /* Build post fields */
270
- rd_kafka_oidc_build_post_fields(rk->rk_conf.sasl.oauthbearer.scope,
271
- &post_fields, &post_fields_size);
948
+ rd_kafka_oidc_client_credentials_build_post_fields(
949
+ rk->rk_conf.sasl.oauthbearer.scope, &post_fields,
950
+ &post_fields_size);
272
951
 
273
952
  token_url = rk->rk_conf.sasl.oauthbearer.token_endpoint_url;
274
953
 
@@ -286,75 +965,129 @@ void rd_kafka_oidc_token_refresh_cb(rd_kafka_t *rk,
286
965
  goto done;
287
966
  }
288
967
 
289
- parsed_token = cJSON_GetObjectItem(json, "access_token");
290
-
291
- if (parsed_token == NULL) {
292
- rd_kafka_oauthbearer_set_token_failure(
293
- rk,
294
- "Expected JSON JWT response with "
295
- "\"access_token\" field");
968
+ jwt_token = rd_kafka_oidc_token_try_validate(json, "access_token", &sub,
969
+ &exp, set_token_errstr,
970
+ sizeof(set_token_errstr));
971
+ if (!jwt_token) {
972
+ rd_kafka_oauthbearer_set_token_failure(rk, set_token_errstr);
296
973
  goto done;
297
974
  }
298
975
 
299
- jwt_token = cJSON_GetStringValue(parsed_token);
300
- if (jwt_token == NULL) {
301
- rd_kafka_oauthbearer_set_token_failure(
302
- rk,
303
- "Expected JSON "
304
- "response as a value string");
305
- goto done;
306
- }
976
+ if (rk->rk_conf.sasl.oauthbearer.extensions_str) {
977
+ extensions =
978
+ rd_string_split(rk->rk_conf.sasl.oauthbearer.extensions_str,
979
+ ',', rd_true, &extension_cnt);
307
980
 
308
- errstr = rd_kafka_jwt_b64_decode_payload(jwt_token, &decoded_payloads);
309
- if (errstr != NULL) {
310
- rd_snprintf(decode_payload_errstr,
311
- sizeof(decode_payload_errstr),
312
- "Failed to decode JWT payload: %s", errstr);
313
- rd_kafka_oauthbearer_set_token_failure(rk,
314
- decode_payload_errstr);
315
- goto done;
981
+ extension_key_value = rd_kafka_conf_kv_split(
982
+ (const char **)extensions, extension_cnt,
983
+ &extension_key_value_cnt);
316
984
  }
317
985
 
318
- payloads = cJSON_Parse(decoded_payloads);
319
- if (payloads == NULL) {
320
- rd_kafka_oauthbearer_set_token_failure(
321
- rk, "Failed to parse JSON JWT payload");
322
- goto done;
323
- }
986
+ if (rd_kafka_oauthbearer_set_token(
987
+ rk, jwt_token, (int64_t)exp * 1000, sub,
988
+ (const char **)extension_key_value, extension_key_value_cnt,
989
+ set_token_errstr,
990
+ sizeof(set_token_errstr)) != RD_KAFKA_RESP_ERR_NO_ERROR)
991
+ rd_kafka_oauthbearer_set_token_failure(rk, set_token_errstr);
324
992
 
325
- jwt_exp = cJSON_GetObjectItem(payloads, "exp");
326
- if (jwt_exp == NULL) {
327
- rd_kafka_oauthbearer_set_token_failure(
328
- rk,
329
- "Expected JSON JWT response with "
330
- "\"exp\" field");
331
- goto done;
332
- }
993
+ done:
994
+ RD_IF_FREE(sub, rd_free);
995
+ RD_IF_FREE(post_fields, rd_free);
996
+ RD_IF_FREE(json, cJSON_Delete);
997
+ RD_IF_FREE(headers, curl_slist_free_all);
998
+ RD_IF_FREE(extensions, rd_free);
999
+ RD_IF_FREE(extension_key_value, rd_free);
1000
+ }
333
1001
 
334
- exp = cJSON_GetNumberValue(jwt_exp);
335
- if (exp <= 0) {
336
- rd_kafka_oauthbearer_set_token_failure(
337
- rk,
338
- "Expected JSON JWT response with "
339
- "valid \"exp\" field");
340
- goto done;
1002
+ /**
1003
+ * @brief Implementation of Oauth/OIDC token refresh callback function
1004
+ * for Azure IMDS,
1005
+ * will receive the JSON response after HTTP(S) GET call to token
1006
+ * provider, then extract the jwt from the JSON response, and
1007
+ * forward it to the broker.
1008
+ */
1009
+ void rd_kafka_oidc_token_metadata_azure_imds_refresh_cb(
1010
+ rd_kafka_t *rk,
1011
+ const char *oauthbearer_config,
1012
+ void *opaque) {
1013
+ const int timeout_s = 20;
1014
+ const int retries = 4;
1015
+ const int retry_ms = 5 * 1000;
1016
+
1017
+ double exp;
1018
+
1019
+ cJSON *json = NULL;
1020
+
1021
+ rd_http_error_t *herr;
1022
+
1023
+ char *jwt_token;
1024
+
1025
+ struct curl_slist *headers = NULL;
1026
+
1027
+ const char *token_endpoint_url_initial = NULL;
1028
+ char *token_endpoint_url = NULL;
1029
+ char *sub = NULL;
1030
+
1031
+ size_t extension_cnt;
1032
+ size_t extension_key_value_cnt = 0;
1033
+
1034
+ char set_token_errstr[512];
1035
+
1036
+ char **extensions = NULL;
1037
+ char **extension_key_value = NULL;
1038
+ char *query = NULL;
1039
+ static char *headers_array[] = {"Metadata: true"};
1040
+
1041
+ if (rd_kafka_terminating(rk))
1042
+ return;
1043
+
1044
+ if (rk->rk_conf.sasl.oauthbearer_config)
1045
+ query = rd_kafka_conf_kv_get(
1046
+ rk->rk_conf.sasl.oauthbearer_config, "query", ',');
1047
+ token_endpoint_url_initial =
1048
+ rk->rk_conf.sasl.oauthbearer.token_endpoint_url;
1049
+ if (!token_endpoint_url_initial)
1050
+ token_endpoint_url_initial =
1051
+ RD_KAFKA_SASL_OAUTHBEARER_METADATA_AUTHENTICATION_URL_AZURE_IMDS;
1052
+ if (query && *query) {
1053
+ token_endpoint_url = rd_http_get_params_append(
1054
+ token_endpoint_url_initial, query);
1055
+
1056
+ if (token_endpoint_url == NULL) {
1057
+ rd_snprintf(
1058
+ set_token_errstr, sizeof(set_token_errstr),
1059
+ "Failed to append params \"%s\" to token endpoint "
1060
+ "URL \"%s\"",
1061
+ query,
1062
+ rk->rk_conf.sasl.oauthbearer.token_endpoint_url);
1063
+ rd_kafka_log(rk, LOG_ERR, "OIDC", "%s",
1064
+ set_token_errstr);
1065
+ rd_kafka_oauthbearer_set_token_failure(
1066
+ rk, set_token_errstr);
1067
+ goto done;
1068
+ }
1069
+ } else {
1070
+ token_endpoint_url = rd_strdup(token_endpoint_url_initial);
341
1071
  }
342
1072
 
343
- jwt_sub = cJSON_GetObjectItem(payloads, "sub");
344
- if (jwt_sub == NULL) {
345
- rd_kafka_oauthbearer_set_token_failure(
346
- rk,
347
- "Expected JSON JWT response with "
348
- "\"sub\" field");
1073
+ herr = rd_http_get_json(rk, token_endpoint_url, headers_array, 1,
1074
+ timeout_s, retries, retry_ms, &json);
1075
+
1076
+ if (unlikely(herr != NULL)) {
1077
+ rd_kafka_log(rk, LOG_ERR, "OIDC",
1078
+ "Failed to retrieve OIDC "
1079
+ "token from \"%s\": %s (%d)",
1080
+ token_endpoint_url, herr->errstr, herr->code);
1081
+ rd_kafka_oauthbearer_set_token_failure(rk, herr->errstr);
1082
+ rd_http_error_destroy(herr);
349
1083
  goto done;
350
1084
  }
351
1085
 
352
- sub = cJSON_GetStringValue(jwt_sub);
353
- if (sub == NULL) {
354
- rd_kafka_oauthbearer_set_token_failure(
355
- rk,
356
- "Expected JSON JWT response with "
357
- "valid \"sub\" field");
1086
+ jwt_token = rd_kafka_oidc_token_try_validate(json, "access_token", &sub,
1087
+ &exp, set_token_errstr,
1088
+ sizeof(set_token_errstr));
1089
+ if (!jwt_token) {
1090
+ rd_kafka_oauthbearer_set_token_failure(rk, set_token_errstr);
358
1091
  goto done;
359
1092
  }
360
1093
 
@@ -376,16 +1109,15 @@ void rd_kafka_oidc_token_refresh_cb(rd_kafka_t *rk,
376
1109
  rd_kafka_oauthbearer_set_token_failure(rk, set_token_errstr);
377
1110
 
378
1111
  done:
379
- RD_IF_FREE(decoded_payloads, rd_free);
380
- RD_IF_FREE(post_fields, rd_free);
1112
+ RD_IF_FREE(sub, rd_free);
381
1113
  RD_IF_FREE(json, cJSON_Delete);
382
1114
  RD_IF_FREE(headers, curl_slist_free_all);
383
1115
  RD_IF_FREE(extensions, rd_free);
384
1116
  RD_IF_FREE(extension_key_value, rd_free);
385
- RD_IF_FREE(payloads, cJSON_Delete);
1117
+ RD_IF_FREE(token_endpoint_url, rd_free);
1118
+ RD_IF_FREE(query, rd_free);
386
1119
  }
387
1120
 
388
-
389
1121
  /**
390
1122
  * @brief Make sure the jwt is able to be extracted from HTTP(S) response.
391
1123
  * The JSON response after HTTP(S) call to token provider will be in
@@ -421,10 +1153,11 @@ static int ut_sasl_oauthbearer_oidc_should_succeed(void) {
421
1153
  cJSON *json = NULL;
422
1154
  char *token;
423
1155
  cJSON *parsed_token;
1156
+ rd_kafka_t *rk = rd_calloc(1, sizeof(*rk));
424
1157
 
425
1158
  RD_UT_BEGIN();
426
1159
 
427
- herr = rd_http_req_init(&hreq, "");
1160
+ herr = rd_http_req_init(rk, &hreq, "");
428
1161
 
429
1162
  RD_UT_ASSERT(!herr,
430
1163
  "Expected initialize to succeed, "
@@ -460,6 +1193,7 @@ static int ut_sasl_oauthbearer_oidc_should_succeed(void) {
460
1193
  rd_http_error_destroy(herr);
461
1194
  rd_http_req_destroy(&hreq);
462
1195
  cJSON_Delete(json);
1196
+ rd_free(rk);
463
1197
 
464
1198
  RD_UT_PASS();
465
1199
  }
@@ -476,10 +1210,11 @@ static int ut_sasl_oauthbearer_oidc_with_empty_key(void) {
476
1210
  rd_http_error_t *herr;
477
1211
  cJSON *json = NULL;
478
1212
  cJSON *parsed_token;
1213
+ rd_kafka_t *rk = rd_calloc(1, sizeof(*rk));
479
1214
 
480
1215
  RD_UT_BEGIN();
481
1216
 
482
- herr = rd_http_req_init(&hreq, "");
1217
+ herr = rd_http_req_init(rk, &hreq, "");
483
1218
  RD_UT_ASSERT(!herr,
484
1219
  "Expected initialization to succeed, "
485
1220
  "but it failed with error code: %d, error string: %s",
@@ -507,6 +1242,7 @@ static int ut_sasl_oauthbearer_oidc_with_empty_key(void) {
507
1242
  rd_http_error_destroy(herr);
508
1243
  cJSON_Delete(json);
509
1244
  cJSON_Delete(parsed_token);
1245
+ rd_free(rk);
510
1246
  RD_UT_PASS();
511
1247
  }
512
1248
 
@@ -526,7 +1262,8 @@ static int ut_sasl_oauthbearer_oidc_post_fields(void) {
526
1262
 
527
1263
  RD_UT_BEGIN();
528
1264
 
529
- rd_kafka_oidc_build_post_fields(scope, &post_fields, &post_fields_size);
1265
+ rd_kafka_oidc_client_credentials_build_post_fields(scope, &post_fields,
1266
+ &post_fields_size);
530
1267
 
531
1268
  RD_UT_ASSERT(expected_post_fields_size == post_fields_size,
532
1269
  "Expected expected_post_fields_size is %" PRIusz
@@ -558,7 +1295,8 @@ static int ut_sasl_oauthbearer_oidc_post_fields_with_empty_scope(void) {
558
1295
 
559
1296
  RD_UT_BEGIN();
560
1297
 
561
- rd_kafka_oidc_build_post_fields(scope, &post_fields, &post_fields_size);
1298
+ rd_kafka_oidc_client_credentials_build_post_fields(scope, &post_fields,
1299
+ &post_fields_size);
562
1300
 
563
1301
  RD_UT_ASSERT(expected_post_fields_size == post_fields_size,
564
1302
  "Expected expected_post_fields_size is %" PRIusz
@@ -587,3 +1325,342 @@ int unittest_sasl_oauthbearer_oidc(void) {
587
1325
  fails += ut_sasl_oauthbearer_oidc_post_fields_with_empty_scope();
588
1326
  return fails;
589
1327
  }
1328
+
1329
+ /**
1330
+ * @brief Test the Base64Url encoding functionality.
1331
+ * Verifies that the encoding correctly handles special characters
1332
+ * and padding removal.
1333
+ */
1334
+ static int ut_sasl_oauthbearer_oidc_jwt_bearer_base64url_encode(void) {
1335
+ /* Test cases with expected inputs and outputs */
1336
+ static const struct {
1337
+ const char *input;
1338
+ const char *expected_output;
1339
+ } test_cases[] = {
1340
+ /* Regular case */
1341
+ {"Hello, world!", "SGVsbG8sIHdvcmxkIQ"},
1342
+ /* Case with padding characters that should be removed */
1343
+ {"test", "dGVzdA"},
1344
+ /* Empty string */
1345
+ {"", ""},
1346
+ /* Special characters that trigger Base64 padding */
1347
+ {"f", "Zg"},
1348
+ {"fo", "Zm8"},
1349
+ {"foo", "Zm9v"},
1350
+ {"foob", "Zm9vYg"},
1351
+ {"fooba", "Zm9vYmE"},
1352
+ {"foobar", "Zm9vYmFy"},
1353
+ /* Characters that produce + and / in standard Base64 */
1354
+ {"\x3E\x3F",
1355
+ "Pj8"}, /* encodes to ">?" in standard Base64 with + and / */
1356
+ };
1357
+ unsigned int i;
1358
+
1359
+ RD_UT_BEGIN();
1360
+
1361
+ for (i = 0; i < RD_ARRAYSIZE(test_cases); i++) {
1362
+ rd_chariov_t input_iov;
1363
+ input_iov.ptr = (char *)test_cases[i].input;
1364
+ input_iov.size = strlen(test_cases[i].input);
1365
+ char *output = rd_base64_encode_str_urlsafe(&input_iov);
1366
+
1367
+ RD_UT_ASSERT(output != NULL,
1368
+ "Expected non-NULL output for input: %s",
1369
+ test_cases[i].input);
1370
+
1371
+ RD_UT_ASSERT(!strcmp(output, test_cases[i].expected_output),
1372
+ "Base64Url encoding failed: expected %s, got %s",
1373
+ test_cases[i].expected_output, output);
1374
+
1375
+ rd_free(output);
1376
+ }
1377
+
1378
+ RD_UT_PASS();
1379
+ }
1380
+
1381
+ /**
1382
+ * @brief Test JWT request body building.
1383
+ * Verifies that the request body is correctly formatted with
1384
+ * the required parameters.
1385
+ */
1386
+ static int ut_sasl_oauthbearer_oidc_jwt_bearer_build_request_body(void) {
1387
+ const char *assertion = "test.jwt.assertion";
1388
+ const char *scope = "test.scope";
1389
+ const char *expected =
1390
+ "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion="
1391
+ "test.jwt.assertion&scope=test.scope";
1392
+ char *body;
1393
+
1394
+ RD_UT_BEGIN();
1395
+
1396
+ body = rd_kafka_oidc_jwt_bearer_build_request_body(assertion, scope);
1397
+
1398
+ RD_UT_ASSERT(body != NULL, "Expected non-NULL request body");
1399
+
1400
+ RD_UT_ASSERT(!strcmp(body, expected),
1401
+ "Request body incorrect: expected '%s', got '%s'",
1402
+ expected, body);
1403
+
1404
+ rd_free(body);
1405
+
1406
+ RD_UT_PASS();
1407
+ }
1408
+
1409
+ /**
1410
+ * @brief Test JWT assertion file parsing.
1411
+ * Verifies that the function correctly reads a JWT from a file.
1412
+ */
1413
+ static int ut_sasl_oauthbearer_oidc_assertion_parse_from_file(void) {
1414
+
1415
+ char tempfile_path[512];
1416
+ FILE *tempfile;
1417
+ const char *test_jwt = "header.payload.signature";
1418
+ char *result;
1419
+
1420
+ RD_UT_BEGIN();
1421
+
1422
+ tempfile = rd_file_mkstemp("rdtmp", "wb", tempfile_path,
1423
+ sizeof(tempfile_path));
1424
+ fprintf(tempfile, "%s", test_jwt);
1425
+ fclose(tempfile);
1426
+
1427
+ /* Test parsing from file */
1428
+ result = rd_kafka_oidc_assertion_read_from_file(tempfile_path);
1429
+ RD_UT_ASSERT(result != NULL,
1430
+ "Expected non-NULL result from parsing file");
1431
+ RD_UT_ASSERT(!strcmp(result, test_jwt),
1432
+ "Incorrect JWT parsed: expected '%s', got '%s'", test_jwt,
1433
+ result);
1434
+
1435
+ rd_free(result);
1436
+
1437
+ /* Test with NULL path */
1438
+ result = rd_kafka_oidc_assertion_read_from_file(NULL);
1439
+ RD_UT_ASSERT(result == NULL, "Expected NULL result with NULL path");
1440
+
1441
+ /* Test with non-existent file */
1442
+ result =
1443
+ rd_kafka_oidc_assertion_read_from_file("/non/existent/file/path");
1444
+ RD_UT_ASSERT(result == NULL,
1445
+ "Expected NULL result with non-existent file");
1446
+
1447
+ remove(tempfile_path);
1448
+
1449
+ RD_UT_PASS();
1450
+ }
1451
+
1452
+ /**
1453
+ * @brief Mock function for testing JWT template processing.
1454
+ * Creates a file with valid JWT template JSON.
1455
+ */
1456
+ static char *ut_create_mock_jwt_template_file(void) {
1457
+ FILE *tempfile;
1458
+ char tempfile_path[512];
1459
+
1460
+ const char *template_json =
1461
+ "{\n"
1462
+ " \"header\": {\n"
1463
+ " \"kid\": \"test-key-id\"\n"
1464
+ " },\n"
1465
+ " \"payload\": {\n"
1466
+ " \"sub\": \"test-subject\",\n"
1467
+ " \"aud\": \"test-audience\"\n"
1468
+ " }\n"
1469
+ "}";
1470
+
1471
+ tempfile = rd_file_mkstemp("rdtmp", "wb", tempfile_path,
1472
+ sizeof(tempfile_path));
1473
+ if (!tempfile)
1474
+ return NULL;
1475
+
1476
+ fprintf(tempfile, "%s", template_json);
1477
+ fclose(tempfile);
1478
+
1479
+ return rd_strdup(tempfile_path);
1480
+ }
1481
+
1482
+ /**
1483
+ * @brief Test JWT template file processing.
1484
+ * Verifies that the function correctly parses header and payload from
1485
+ * template.
1486
+ */
1487
+ static int ut_sasl_oauthbearer_oidc_assertion_process_template_file(void) {
1488
+ char *template_path;
1489
+ rd_kafka_t *rk;
1490
+ cJSON *header = NULL, *payload = NULL;
1491
+ int result;
1492
+
1493
+ RD_UT_BEGIN();
1494
+
1495
+ rk = rd_calloc(1, sizeof(*rk));
1496
+
1497
+ template_path = ut_create_mock_jwt_template_file();
1498
+ RD_UT_ASSERT(template_path != NULL, "Failed to create template file");
1499
+
1500
+ /* Test template processing */
1501
+ result = rd_kafka_oidc_assertion_parse_template_file(rk, template_path,
1502
+ &header, &payload);
1503
+ RD_UT_ASSERT(result == 0, "Expected success from template processing");
1504
+ RD_UT_ASSERT(header != NULL, "Expected non-NULL header JSON");
1505
+ RD_UT_ASSERT(payload != NULL, "Expected non-NULL payload JSON");
1506
+
1507
+ /* Verify header contents */
1508
+ cJSON *kid = cJSON_GetObjectItem(header, "kid");
1509
+ RD_UT_ASSERT(kid != NULL, "Expected kid in header");
1510
+ RD_UT_ASSERT(cJSON_IsString(kid), "Expected kid to be string");
1511
+ RD_UT_ASSERT(!strcmp(cJSON_GetStringValue(kid), "test-key-id"),
1512
+ "Incorrect kid value");
1513
+
1514
+ /* Verify payload contents */
1515
+ cJSON *sub = cJSON_GetObjectItem(payload, "sub");
1516
+ RD_UT_ASSERT(sub != NULL, "Expected sub in payload");
1517
+ RD_UT_ASSERT(cJSON_IsString(sub), "Expected sub to be string");
1518
+ RD_UT_ASSERT(!strcmp(cJSON_GetStringValue(sub), "test-subject"),
1519
+ "Incorrect sub value");
1520
+
1521
+ cJSON *aud = cJSON_GetObjectItem(payload, "aud");
1522
+ RD_UT_ASSERT(aud != NULL, "Expected aud in payload");
1523
+ RD_UT_ASSERT(cJSON_IsString(aud), "Expected aud to be string");
1524
+ RD_UT_ASSERT(!strcmp(cJSON_GetStringValue(aud), "test-audience"),
1525
+ "Incorrect aud value");
1526
+
1527
+ /* Test with non-existent file */
1528
+ cJSON_Delete(header);
1529
+ cJSON_Delete(payload);
1530
+ header = NULL;
1531
+ payload = NULL;
1532
+
1533
+ result = rd_kafka_oidc_assertion_parse_template_file(
1534
+ rk, "/non/existent/file", &header, &payload);
1535
+ RD_UT_ASSERT(result == -1, "Expected failure with non-existent file");
1536
+ RD_UT_ASSERT(header == NULL,
1537
+ "Expected NULL header with failed processing");
1538
+ RD_UT_ASSERT(payload == NULL,
1539
+ "Expected NULL payload with failed processing");
1540
+
1541
+ unlink(template_path);
1542
+ rd_free(template_path);
1543
+ rd_free(rk);
1544
+ if (header)
1545
+ cJSON_Delete(header);
1546
+ if (payload)
1547
+ cJSON_Delete(payload);
1548
+
1549
+ RD_UT_PASS();
1550
+ }
1551
+
1552
+ /**
1553
+ * @brief Test JWT assertion creation with minimal approach.
1554
+ * Creates a simplified test that validates the format of the created
1555
+ * JWT.
1556
+ */
1557
+ static int ut_sasl_oauthbearer_oidc_assertion_create(void) {
1558
+ rd_kafka_t *rk;
1559
+ char *private_key_pem;
1560
+ char *jwt;
1561
+ char *header_part, *payload_part, *signature_part;
1562
+ char *dot1, *dot2;
1563
+
1564
+ RD_UT_BEGIN();
1565
+
1566
+ rk = rd_calloc(1, sizeof(*rk));
1567
+
1568
+ /* Random key for signing */
1569
+ private_key_pem =
1570
+ "-----BEGIN PRIVATE KEY-----\n"
1571
+ "MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuBS7qG5Cd2voa\n"
1572
+ "7nSU2xaDbe6QOYU2P4bIY58SKHbFyq1iB517r61ImsWD+UfZuVxCqXRaWdxxnG/D\n"
1573
+ "5VGTQzBOZYlgSYxdJ1KvITXO8kj5i2zBT/LI9R9MTQ7nLFh+vQm1aM8Ts1PmA5t9\n"
1574
+ "zFtR9B8RfqN9kbt+2LnLY57aJxEkFC3D89D0WWT97UJWKo7/vxMqp9K9uAIL2Efo\n"
1575
+ "5rp9qwyPbx9LmTbfZ8Vog6mG6tAQQHSUqw0PnfhADCVCkYtkzYcyDZy3qZQFu1bY\n"
1576
+ "KuuMoMjssyCUL5tTHyNZju0p3Z0bSfOV/nkqHpSSjHKCeQkSKS18/7In6cfY/M4k\n"
1577
+ "8rM4HWkdAgMBAAECggEAFsTo2YrXxj/Dn8h5ioyMCpBUuZw9GNcBDLE0PAz9VW3q\n"
1578
+ "d7wlV+ypkKlnlJgGVa+SKcrARZ4iYN8mJIyZutn8tRVF/0pASmP9xppizvwWnkgm\n"
1579
+ "57hNPQwNl08x1v+PaK3VWl4nUh2RqbPpIXGetT9q3UAjpiduT++Nh9Y2D7cy3/Ro\n"
1580
+ "ritnpBDs1R6y5J3rxiE1s8kLYwhDRCPsgUg/ZtKPDTTFz42ArrFeqM91FmjHYP3t\n"
1581
+ "p9Uh6CIZ80D6CsMX/TnZFfhKe6EvKBSl4W6tcdFlnXW52fm/670iKSmcJ09+fzPO\n"
1582
+ "T1BLrkXGv51bFnlvUyJqQGVEv5+0+HUX/oTpTknMQQKBgQDbYhqip5e8r1f5v32B\n"
1583
+ "k1r3xtEiWU2mZoTHJu6bVeuigzVhz4pTMVZChElJ4QnhwwO0t5Oe4Su1MZtjMRw7\n"
1584
+ "qIE+YM2pXss25LRXbmWItuRWINzpe8omlxQSOj2tNO/67l0P4vmmrT5wkU2cG6TR\n"
1585
+ "ddzorO3NDA4MY4+Xdli+SHXwUQKBgQDLEMqlwyvaGjuZ30l6F13fWnEt9PNCtJsa\n"
1586
+ "nsdKJKyFMThdysY/PK40o2GTRRhgYa2jigN3OCYSSznRRZRlqznqL1bOLlYV6zS8\n"
1587
+ "TGhdLXuApyLAjZYIK4RtZJYGR9+yg8rH13uNektgW8KnHh5Ko/ptRVoEukf3SBsh\n"
1588
+ "f0Fib3ylDQKBgE11Bth0+bMJ6bLpNEPiphSjosVQ6ISe37R8/3Pi0y5uyxM8tqcG\n"
1589
+ "3WDg2gt2pAmM1CsjQcCv2cHAwQ81kLVTmkZO4W4yZOd9ulrARKMPh/EM61KYfVhA\n"
1590
+ "sTp6S7py3WQocr0gM2rw8gHGm7NJY1j9F0EjhVaHMhKXuGQOyehtJw7xAoGAPwuA\n"
1591
+ "jwRQSg+Y74XmbxRwHZcbynPhTpV6DkK7huZp9ZQ5ds0szZdOUqNi+PEbx1isKzj/\n"
1592
+ "KHVzRHy8f5+FmicV/QIjhjHWokl6/vcN89faHzBE1tleejzgiYIQHfUUm3zVaUQa\n"
1593
+ "ZOtSGaGDhpUQPIY6itBcSVl4XGqzmavDpgcNAMUCgYBFFGtG+RbSySzKfRUp3vc5\n"
1594
+ "8YqIdrtXfW9gc9s1+Pw8wfgrY0Rrvy+e3ClSwgGENxgxBvWvhzq2m0S8x2jdLAl1\n"
1595
+ "b+VLGCOpUvS4iN2yrHkoHS7BSW40wLuVooJUAaNOIEPqiv1JC75q2dhTRrANp6WB\n"
1596
+ "bm+7yWVTNlXYuKQqtuOkNQ==\n"
1597
+ "-----END PRIVATE KEY-----\n";
1598
+
1599
+ jwt = rd_kafka_oidc_assertion_create(
1600
+ rk, private_key_pem, NULL, NULL,
1601
+ RD_KAFKA_SASL_OAUTHBEARER_ASSERTION_ALGORITHM_RS256, NULL,
1602
+ "test-subject", "test-issuer", "test-audience", 2, 300, rd_true);
1603
+
1604
+ RD_UT_ASSERT(jwt != NULL, "Failed to create JWT assertion");
1605
+
1606
+ dot1 = strchr(jwt, '.');
1607
+ RD_UT_ASSERT(dot1 != NULL, "JWT missing first dot separator");
1608
+
1609
+ dot2 = strchr(dot1 + 1, '.');
1610
+ RD_UT_ASSERT(dot2 != NULL, "JWT missing second dot separator");
1611
+
1612
+ header_part = rd_strndup(jwt, dot1 - jwt);
1613
+ payload_part = rd_strndup(dot1 + 1, dot2 - (dot1 + 1));
1614
+ signature_part = rd_strdup(dot2 + 1);
1615
+
1616
+ RD_UT_ASSERT(strlen(header_part) > 0, "JWT header part is empty");
1617
+ RD_UT_ASSERT(strlen(payload_part) > 0, "JWT payload part is empty");
1618
+ RD_UT_ASSERT(strlen(signature_part) > 0, "JWT signature part is empty");
1619
+
1620
+ RD_UT_ASSERT(!strchr(header_part, '='),
1621
+ "JWT header contains padding character");
1622
+ RD_UT_ASSERT(!strchr(payload_part, '='),
1623
+ "JWT payload contains padding character");
1624
+ RD_UT_ASSERT(!strchr(signature_part, '='),
1625
+ "JWT signature contains padding character");
1626
+
1627
+ RD_UT_ASSERT(!strchr(header_part, '+'),
1628
+ "JWT header contains '+' character");
1629
+ RD_UT_ASSERT(!strchr(header_part, '/'),
1630
+ "JWT header contains '/' character");
1631
+ RD_UT_ASSERT(!strchr(payload_part, '+'),
1632
+ "JWT payload contains '+' character");
1633
+ RD_UT_ASSERT(!strchr(payload_part, '/'),
1634
+ "JWT payload contains '/' character");
1635
+ RD_UT_ASSERT(!strchr(signature_part, '+'),
1636
+ "JWT signature contains '+' character");
1637
+ RD_UT_ASSERT(!strchr(signature_part, '/'),
1638
+ "JWT signature contains '/' character");
1639
+
1640
+ rd_free(header_part);
1641
+ rd_free(payload_part);
1642
+ rd_free(signature_part);
1643
+ rd_free(jwt);
1644
+ rd_free(rk);
1645
+
1646
+ RD_UT_PASS();
1647
+ }
1648
+
1649
+ int unittest_sasl_oauthbearer_oidc_jwt_bearer(void) {
1650
+ int fails = 0;
1651
+
1652
+ fails += ut_sasl_oauthbearer_oidc_jwt_bearer_base64url_encode();
1653
+ fails += ut_sasl_oauthbearer_oidc_jwt_bearer_build_request_body();
1654
+
1655
+ return fails;
1656
+ }
1657
+
1658
+ int unittest_sasl_oauthbearer_oidc_assertion(void) {
1659
+ int fails = 0;
1660
+
1661
+ fails += ut_sasl_oauthbearer_oidc_assertion_parse_from_file();
1662
+ fails += ut_sasl_oauthbearer_oidc_assertion_process_template_file();
1663
+ fails += ut_sasl_oauthbearer_oidc_assertion_create();
1664
+
1665
+ return fails;
1666
+ }