From df2418a898a6eea001350a05e2724bc6a1112789 Mon Sep 17 00:00:00 2001 From: Dimitris Christodoulou Date: Thu, 29 Jan 2026 13:58:17 +0000 Subject: [PATCH] RCBC-522: Support for JWT authentication --- ext/rcb_backend.cxx | 47 ++++++++++++++++++++------- lib/couchbase/authenticator.rb | 14 ++++++++ lib/couchbase/cluster.rb | 11 ++++++- lib/couchbase/options.rb | 2 +- lib/couchbase/protostellar/cluster.rb | 3 ++ 5 files changed, 63 insertions(+), 14 deletions(-) diff --git a/ext/rcb_backend.cxx b/ext/rcb_backend.cxx index 63136feb..426a1f33 100644 --- a/ext/rcb_backend.cxx +++ b/ext/rcb_backend.cxx @@ -179,18 +179,21 @@ cb_Backend_allocate(VALUE klass) } auto -construct_authenticator(VALUE credentials) - -> std::variant +construct_authenticator(VALUE credentials) -> std::variant { cb_check_type(credentials, T_HASH); static const auto sym_certificate_path{ rb_id2sym(rb_intern("certificate_path")) }; static const auto sym_key_path{ rb_id2sym(rb_intern("key_path")) }; + static const auto sym_jwt{ rb_id2sym(rb_intern("jwt")) }; const VALUE certificate_path = rb_hash_aref(credentials, sym_certificate_path); const VALUE key_path = rb_hash_aref(credentials, sym_key_path); + const VALUE jwt = rb_hash_aref(credentials, sym_jwt); - if (NIL_P(certificate_path) || NIL_P(key_path)) { + if (NIL_P(certificate_path) && NIL_P(key_path) && NIL_P(jwt)) { static const auto sym_username = rb_id2sym(rb_intern("username")); static const auto sym_password = rb_id2sym(rb_intern("password")); @@ -206,20 +209,26 @@ construct_authenticator(VALUE credentials) }; } - cb_check_type(certificate_path, T_STRING); - cb_check_type(key_path, T_STRING); + if (NIL_P(jwt)) { + cb_check_type(certificate_path, T_STRING); + cb_check_type(key_path, T_STRING); - return couchbase::certificate_authenticator{ - cb_string_new(certificate_path), - cb_string_new(key_path), + return couchbase::certificate_authenticator{ + cb_string_new(certificate_path), + cb_string_new(key_path), + }; + } + + cb_check_type(jwt, T_STRING); + return couchbase::jwt_authenticator{ + cb_string_new(jwt), }; } auto construct_cluster_options(VALUE credentials, bool tls_enabled) -> couchbase::cluster_options { - std::variant - authenticator = construct_authenticator(credentials); + auto authenticator = construct_authenticator(credentials); if (std::holds_alternative(authenticator)) { return couchbase::cluster_options{ @@ -227,6 +236,18 @@ construct_cluster_options(VALUE credentials, bool tls_enabled) -> couchbase::clu }; } + if (std::holds_alternative(authenticator)) { + if (!tls_enabled) { + throw ruby_exception( + exc_invalid_argument(), + "JWT authenticator requires TLS connection, check the connection string"); + } + + return couchbase::cluster_options{ + std::get(std::move(authenticator)), + }; + } + if (!tls_enabled) { throw ruby_exception( exc_invalid_argument(), @@ -572,13 +593,15 @@ cb_Backend_update_credentials(VALUE self, VALUE credentials) auto cluster = cb_backend_to_public_api_cluster(self); try { - std::variant - authenticator = construct_authenticator(credentials); + auto authenticator = construct_authenticator(credentials); couchbase::error err{}; if (std::holds_alternative(authenticator)) { err = cluster.set_authenticator( std::get(std::move(authenticator))); + } else if (std::holds_alternative(authenticator)) { + err = + cluster.set_authenticator(std::get(std::move(authenticator))); } else { err = cluster.set_authenticator( std::get(std::move(authenticator))); diff --git a/lib/couchbase/authenticator.rb b/lib/couchbase/authenticator.rb index c6e08a6b..2831f7bb 100644 --- a/lib/couchbase/authenticator.rb +++ b/lib/couchbase/authenticator.rb @@ -63,4 +63,18 @@ def initialize(certificate_path, key_path) @key_path = key_path end end + + # Authenticator using a JSON Web Token (JWT) + # + # @!macro uncommitted + class JWTAuthenticator + attr_accessor :token + + # Creates a new authenticator with a JSON Web Token (JWT) + # + # @param [String] token the JWT + def initialize(token) + @token = token + end + end end diff --git a/lib/couchbase/cluster.rb b/lib/couchbase/cluster.rb index fe3f1cfd..d668ecb5 100644 --- a/lib/couchbase/cluster.rb +++ b/lib/couchbase/cluster.rb @@ -96,6 +96,9 @@ def bucket(name) Bucket.new(@backend, name, @observability) end + # Updates the authenticator used for this cluster connection + # + # @param [PasswordAuthenticator, CertificateAuthenticator, JWTAuthenticator] authenticator the new authenticator def update_authenticator(authenticator) credentials = {} @@ -114,6 +117,10 @@ def update_authenticator(authenticator) credentials[:key_path] = authenticator.key_path raise ArgumentError, "missing key path" unless credentials[:key_path] + when JWTAuthenticator + credentials[:jwt] = authenticator.token + raise ArgumentError, "missing token" unless credentials[:jwt] + else raise ArgumentError, "argument must be an authenticator" end @@ -396,7 +403,9 @@ def initialize(connection_string, *args) credentials[:key_path] = authenticator.key_path raise ArgumentError, "missing key path" unless credentials[:key_path] - + when JWTAuthenticator + credentials[:jwt] = authenticator.token + raise ArgumentError, "missing token" unless credentials[:jwt] else raise ArgumentError, "options must have authenticator configured" end diff --git a/lib/couchbase/options.rb b/lib/couchbase/options.rb index 43f69df0..1e64c2d5 100644 --- a/lib/couchbase/options.rb +++ b/lib/couchbase/options.rb @@ -1672,7 +1672,7 @@ def initialize(get_options: Get.new, # @see .Cluster # class Cluster - attr_accessor :authenticator # @return [PasswordAuthenticator, CertificateAuthenticator] + attr_accessor :authenticator # @return [PasswordAuthenticator, CertificateAuthenticator, JWTAuthenticator] attr_accessor :preferred_server_group # @return [String] diff --git a/lib/couchbase/protostellar/cluster.rb b/lib/couchbase/protostellar/cluster.rb index c4dd4526..4d31471a 100644 --- a/lib/couchbase/protostellar/cluster.rb +++ b/lib/couchbase/protostellar/cluster.rb @@ -79,6 +79,9 @@ def self.connect(connection_string_or_config, *args) when Couchbase::CertificateAuthenticator raise Couchbase::Error::FeatureNotAvailable, "The #{Couchbase::Protostellar::NAME} protocol does not support the CertificateAuthenticator" + when Couchbase::JWTAuthenticator + raise Couchbase::Error::FeatureNotAvailable, + "The #{Couchbase::Protostellar::NAME} protocol does not support the JWTAuthenticator" else raise ArgumentError, "options must have authenticator configured" end