From 17b66fd7d8fcf6970c58877d9b66645a6e3ed8aa Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 26 Sep 2025 08:06:05 -0400 Subject: [PATCH 01/11] add binary op to BSON DSL --- src/common/src/bson-dsl.md | 4 ++++ src/common/src/common-bson-dsl-private.h | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/src/common/src/bson-dsl.md b/src/common/src/bson-dsl.md index b1311fc2414..21db66169c9 100644 --- a/src/common/src/bson-dsl.md +++ b/src/common/src/bson-dsl.md @@ -261,6 +261,10 @@ Generate an integral value from the given C integer expression. Generate a UTF-8 value from the given null-terminated character array beginning at `zstr`. +#### `binary(bson_subtype_t subtype, const uint8_t *binary, uint32_t length)` + +Generate a binary value from a subtype, pointer, and length. + #### `oid(const bson_oid_t* oid)` diff --git a/src/common/src/common-bson-dsl-private.h b/src/common/src/common-bson-dsl-private.h index 18b39936c4f..38977f2e4b7 100644 --- a/src/common/src/common-bson-dsl-private.h +++ b/src/common/src/common-bson-dsl-private.h @@ -275,6 +275,12 @@ BSON_IF_GNU_LIKE(_Pragma("GCC diagnostic ignored \"-Wshadow\"")) } \ _bsonDSL_end +#define _bsonValueOperation_binary(SubType, Data, Len) \ + if (!bson_append_binary(_bsonBuildAppendArgs, (SubType), (Data), (Len))) { \ + bsonBuildError = "Error while appending binary(" _bsonDSL_str(Data) ")"; \ + } else \ + ((void)0) + /// Insert the given BSON document into the parent document in-place #define _bsonDocOperation_insert(OtherBSON, Pred) \ _bsonDSL_begin("Insert other document: [%s]", _bsonDSL_str(OtherBSON)); \ From be811941e0f32f7323ea062a8ab0e7a5791c6aa6 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 26 Sep 2025 08:26:47 -0400 Subject: [PATCH 02/11] clarify timeout is a duration, not an absolute time point --- .../mongoc_oidc_callback_params_get_timeout.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst b/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst index 36919ee0fba..61de9cdb246 100644 --- a/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst +++ b/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst @@ -13,6 +13,22 @@ Synopsis Return a value comparable with :symbol:`bson_get_monotonic_time()` to determine when a timeout must occur. +The return value is an absolute time point, not a duration. A callback can signal a timeout error using. Example: + +.. code-block:: c + + mongoc_oidc_credential_t * + example_callback_fn (mongoc_oidc_callback_params_t *params) { + const int64_t *timeout = mongoc_oidc_callback_params_get_timeout (params); + + // NULL means "infinite" timeout. + if (timeout && bson_get_monotonic_time () > *timeout) { + return mongoc_oidc_callback_params_cancel_with_timeout (params); + } + + // ... + } + A ``NULL`` (unset) return value means "infinite" timeout. Parameters @@ -34,3 +50,4 @@ The pointed-to ``int64_t`` is only valid for the duration of the invocation of t - :symbol:`mongoc_oidc_callback_params_t` - :symbol:`mongoc_oidc_callback_t` + - :symbol:`mongoc_oidc_callback_params_cancel_with_timeout` From c57816da73bbfc23f450b4bfd7ea025c92b5e8e2 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 26 Sep 2025 08:26:52 -0400 Subject: [PATCH 03/11] CDRIVER-4689 implement machine flow for OIDC --- .../config_generator/components/oidc.py | 74 ++ .evergreen/generated_configs/task_groups.yml | 32 +- .evergreen/generated_configs/tasks.yml | 15 + .evergreen/generated_configs/variants.yml | 6 + .evergreen/scripts/run-tests.sh | 8 + CONTRIBUTING.md | 2 + src/libmongoc/CMakeLists.txt | 2 + .../mongoc_client_pool_set_oidc_callback.rst | 33 + src/libmongoc/doc/mongoc_client_pool_t.rst | 1 + .../doc/mongoc_client_set_oidc_callback.rst | 32 + src/libmongoc/doc/mongoc_client_t.rst | 1 + ...ongoc_oidc_callback_params_get_timeout.rst | 5 +- .../doc/mongoc_oidc_callback_params_t.rst | 2 +- src/libmongoc/doc/mongoc_oidc_callback_t.rst | 12 +- src/libmongoc/src/mongoc/mongoc-client-pool.c | 21 + src/libmongoc/src/mongoc/mongoc-client-pool.h | 3 + src/libmongoc/src/mongoc/mongoc-client.c | 22 + src/libmongoc/src/mongoc/mongoc-client.h | 4 + .../src/mongoc/mongoc-cluster-oidc-private.h | 48 + .../src/mongoc/mongoc-cluster-oidc.c | 216 +++++ .../src/mongoc/mongoc-cluster-private.h | 2 + src/libmongoc/src/mongoc/mongoc-cluster.c | 154 +++- .../src/mongoc/mongoc-error-private.h | 7 +- src/libmongoc/src/mongoc/mongoc-error.c | 12 + .../src/mongoc/mongoc-oidc-cache-private.h | 18 + src/libmongoc/src/mongoc/mongoc-oidc-cache.c | 37 + .../src/mongoc/mongoc-topology-private.h | 6 + .../mongoc/mongoc-topology-scanner-private.h | 13 +- .../src/mongoc/mongoc-topology-scanner.c | 36 +- src/libmongoc/src/mongoc/mongoc-topology.c | 5 + src/libmongoc/src/mongoc/mongoc-util.c | 3 + .../auth/{ => legacy}/connection-string.json | 0 .../auth/unified/mongodb-oidc-no-retry.json | 422 +++++++++ src/libmongoc/tests/test-libmongoc-main.c | 1 + src/libmongoc/tests/test-libmongoc.c | 47 + src/libmongoc/tests/test-libmongoc.h | 6 + .../tests/test-mongoc-connection-uri.c | 2 +- src/libmongoc/tests/test-mongoc-oidc-cache.c | 26 + src/libmongoc/tests/test-mongoc-oidc.c | 866 ++++++++++++++++++ src/libmongoc/tests/unified/entity-map.c | 53 +- src/libmongoc/tests/unified/runner.c | 29 +- 41 files changed, 2235 insertions(+), 49 deletions(-) create mode 100644 .evergreen/config_generator/components/oidc.py create mode 100644 src/libmongoc/doc/mongoc_client_pool_set_oidc_callback.rst create mode 100644 src/libmongoc/doc/mongoc_client_set_oidc_callback.rst create mode 100644 src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h create mode 100644 src/libmongoc/src/mongoc/mongoc-cluster-oidc.c rename src/libmongoc/tests/json/auth/{ => legacy}/connection-string.json (100%) create mode 100644 src/libmongoc/tests/json/auth/unified/mongodb-oidc-no-retry.json create mode 100644 src/libmongoc/tests/test-mongoc-oidc.c diff --git a/.evergreen/config_generator/components/oidc.py b/.evergreen/config_generator/components/oidc.py new file mode 100644 index 00000000000..73f51c296f2 --- /dev/null +++ b/.evergreen/config_generator/components/oidc.py @@ -0,0 +1,74 @@ +from shrub.v3.evg_build_variant import BuildVariant +from shrub.v3.evg_command import EvgCommandType, ec2_assume_role, KeyValueParam, expansions_update +from shrub.v3.evg_task import EvgTask, EvgTaskRef +from shrub.v3.evg_task_group import EvgTaskGroup + +from config_generator.components.funcs.run_tests import RunTests +from config_generator.components.funcs.fetch_det import FetchDET +from config_generator.components.funcs.fetch_source import FetchSource +from config_generator.components.sasl.openssl import SaslCyrusOpenSSLCompile +from config_generator.etc.utils import bash_exec +from config_generator.etc.distros import find_small_distro + + +def task_groups(): + return [ + EvgTaskGroup( + name='test-oidc-task-group', + tasks=['oidc-auth-test-task'], + setup_group_can_fail_task=True, + setup_group_timeout_secs=60 * 60, # 1 hour + teardown_group_can_fail_task=True, + teardown_group_timeout_secs=60 * 60, # 1 hour + setup_group=[ + FetchDET.call(), + ec2_assume_role(role_arn='${aws_test_secrets_role}'), + bash_exec( + command_type=EvgCommandType.SETUP, + include_expansions_in_env=['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN'], + script='./drivers-evergreen-tools/.evergreen/auth_oidc/setup.sh', + ), + ], + teardown_group=[ + bash_exec( + command_type=EvgCommandType.SETUP, + script='./drivers-evergreen-tools/.evergreen/auth_oidc/teardown.sh', + ) + ], + ) + ] + + +def tasks(): + return [ + EvgTask( + name='oidc-auth-test-task', + run_on=[find_small_distro('ubuntu2404').name], + commands=[ + FetchSource.call(), + SaslCyrusOpenSSLCompile.call(), + expansions_update( + updates=[ + KeyValueParam(key='CC', value='clang'), + # OIDC test servers support both OIDC and user/password. + KeyValueParam(key='AUTH', value='auth'), # Use user/password for default test clients. + KeyValueParam(key='OIDC', value='oidc'), # Enable OIDC tests. + KeyValueParam(key='MONGODB_VERSION', value='latest'), + KeyValueParam(key='TOPOLOGY', value='replica_set'), + ] + ), + RunTests.call(), + ], + ) + ] + + +def variants(): + return [ + BuildVariant( + name='oidc', + display_name='OIDC', + run_on=[find_small_distro('ubuntu2404').name], + tasks=[EvgTaskRef(name='test-oidc-task-group')], + ), + ] diff --git a/.evergreen/generated_configs/task_groups.yml b/.evergreen/generated_configs/task_groups.yml index 96d97eada4a..46518092f86 100644 --- a/.evergreen/generated_configs/task_groups.yml +++ b/.evergreen/generated_configs/task_groups.yml @@ -1 +1,31 @@ -task_groups: [] +task_groups: + - name: test-oidc-task-group + setup_group: + - func: fetch-det + - command: ec2.assume_role + params: + role_arn: ${aws_test_secrets_role} + - command: subprocess.exec + type: setup + params: + binary: bash + include_expansions_in_env: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + args: + - -c + - ./drivers-evergreen-tools/.evergreen/auth_oidc/setup.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 3600 + tasks: + - oidc-auth-test-task + teardown_group: + - command: subprocess.exec + type: setup + params: + binary: bash + args: + - -c + - ./drivers-evergreen-tools/.evergreen/auth_oidc/teardown.sh + teardown_group_timeout_secs: 3600 diff --git a/.evergreen/generated_configs/tasks.yml b/.evergreen/generated_configs/tasks.yml index 1e91d543f65..e368108ddc3 100644 --- a/.evergreen/generated_configs/tasks.yml +++ b/.evergreen/generated_configs/tasks.yml @@ -4206,6 +4206,21 @@ tasks: args: - -c - .evergreen/scripts/run-mock-server-tests.sh + - name: oidc-auth-test-task + run_on: + - ubuntu2404-small + commands: + - func: fetch-source + - func: sasl-cyrus-openssl-compile + - command: expansions.update + params: + updates: + - { key: CC, value: clang } + - { key: AUTH, value: auth } + - { key: OIDC, value: oidc } + - { key: MONGODB_VERSION, value: latest } + - { key: TOPOLOGY, value: replica_set } + - func: run-tests - name: openssl-compat-1.0.2-shared-ubuntu2404-gcc run_on: ubuntu2404-large tags: [openssl-compat, openssl-1.0.2, openssl-shared, ubuntu2404, gcc] diff --git a/.evergreen/generated_configs/variants.yml b/.evergreen/generated_configs/variants.yml index 1ca8041d78a..cc35dc56648 100644 --- a/.evergreen/generated_configs/variants.yml +++ b/.evergreen/generated_configs/variants.yml @@ -253,6 +253,12 @@ buildvariants: SANITIZE: address,undefined tasks: - name: mock-server-test + - name: oidc + display_name: OIDC + run_on: + - ubuntu2404-small + tasks: + - name: test-oidc-task-group - name: openssl-compat-matrix display_name: OpenSSL Compatibility Matrix tasks: diff --git a/.evergreen/scripts/run-tests.sh b/.evergreen/scripts/run-tests.sh index 00c71d49bbd..ab57ba9fb51 100755 --- a/.evergreen/scripts/run-tests.sh +++ b/.evergreen/scripts/run-tests.sh @@ -22,6 +22,7 @@ check_var_opt SINGLE_MONGOS_LB_URI check_var_opt SKIP_CRYPT_SHARED_LIB check_var_opt SSL "nossl" check_var_opt URI +check_var_opt OIDC "nooidc" declare script_dir script_dir="$(to_absolute "$(dirname "${BASH_SOURCE[0]}")")" @@ -154,6 +155,13 @@ if [[ "${DNS}" != "nodns" ]]; then fi fi +if [[ "${OIDC}" != "nooidc" ]]; then + export MONGOC_TEST_OIDC="ON" + # Only run OIDC tests. + test_args+=("-l" "/oidc/*") + test_args+=("-l" "/auth/unified/*") +fi + wait_for_server() { declare name="${1:?"wait_for_server requires a server name"}" declare port="${2:?"wait_for_server requires a server port"}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 52a88fac742..a75dcdaa659 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -320,6 +320,8 @@ To run test cases with large allocations, set: * `MONGOC_TEST_LARGE_ALLOCATIONS=on` This may result in sudden test suite termination due to allocation failure. Use with caution. +* `MONGOC_TEST_OIDC=on` to test OIDC using a test environment described [here](https://github.com/mongodb-labs/drivers-evergreen-tools/tree/d7a7337b384392a09fbe7fc80a7244e6f1226c18/.evergreen/auth_oidc). + All tests should pass before submitting a patch. ## Configuring the test runner diff --git a/src/libmongoc/CMakeLists.txt b/src/libmongoc/CMakeLists.txt index 36018b7efd6..06a541df821 100644 --- a/src/libmongoc/CMakeLists.txt +++ b/src/libmongoc/CMakeLists.txt @@ -557,6 +557,7 @@ set (MONGOC_SOURCES ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-client-side-encryption.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-cluster.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-cluster-aws.c + ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-cluster-oidc.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-cluster-sasl.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-collection.c ${PROJECT_SOURCE_DIR}/src/mongoc/mongoc-compression.c @@ -1092,6 +1093,7 @@ set (test-libmongoc-sources ${PROJECT_SOURCE_DIR}/tests/test-mongoc-long-namespace.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-max-staleness.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-mongos-pinning.c + ${PROJECT_SOURCE_DIR}/tests/test-mongoc-oidc.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-oidc-callback.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-oidc-cache.c ${PROJECT_SOURCE_DIR}/tests/test-mongoc-opts.c diff --git a/src/libmongoc/doc/mongoc_client_pool_set_oidc_callback.rst b/src/libmongoc/doc/mongoc_client_pool_set_oidc_callback.rst new file mode 100644 index 00000000000..77b6febc60b --- /dev/null +++ b/src/libmongoc/doc/mongoc_client_pool_set_oidc_callback.rst @@ -0,0 +1,33 @@ +:man_page: mongoc_client_pool_set_oidc_callback + +mongoc_client_pool_set_oidc_callback() +====================================== + +Synopsis +-------- + +.. code-block:: c + + bool + mongoc_client_pool_set_oidc_callback(mongoc_client_pool_t *pool, + const mongoc_oidc_callback_t *callback); + +Register a callback for the ``MONGODB-OIDC`` authentication mechanism. + +Parameters +---------- + +* ``pool``: A :symbol:`mongoc_client_pool_t`. +* ``callback``: A :symbol:`mongoc_oidc_callback_t`. + +Returns +------- + +Returns true on success. Returns false and logs on error. + +.. include:: includes/mongoc_client_pool_call_once.txt + +.. seealso:: + | :doc:`mongoc_client_set_oidc_callback` for setting a callback on a single-threaded client. + | :doc:`mongoc_oidc_callback_t` + | :doc:`mongoc_oidc_callback_params_t` diff --git a/src/libmongoc/doc/mongoc_client_pool_t.rst b/src/libmongoc/doc/mongoc_client_pool_t.rst index 6b9357d1180..0179f02104b 100644 --- a/src/libmongoc/doc/mongoc_client_pool_t.rst +++ b/src/libmongoc/doc/mongoc_client_pool_t.rst @@ -40,6 +40,7 @@ Example mongoc_client_pool_set_apm_callbacks mongoc_client_pool_set_appname mongoc_client_pool_set_error_api + mongoc_client_pool_set_oidc_callback mongoc_client_pool_set_server_api mongoc_client_pool_set_ssl_opts mongoc_client_pool_set_structured_log_opts diff --git a/src/libmongoc/doc/mongoc_client_set_oidc_callback.rst b/src/libmongoc/doc/mongoc_client_set_oidc_callback.rst new file mode 100644 index 00000000000..17c4602a4e2 --- /dev/null +++ b/src/libmongoc/doc/mongoc_client_set_oidc_callback.rst @@ -0,0 +1,32 @@ +:man_page: mongoc_client_set_oidc_callback + +mongoc_client_set_oidc_callback() +================================= + +Synopsis +-------- + +.. code-block:: c + + bool + mongoc_client_set_oidc_callback(mongoc_client_t *client, + const mongoc_oidc_callback_t *callback); + +Register a callback for the ``MONGODB-OIDC`` authentication mechanism. + +Parameters +---------- + +* ``client``: A :symbol:`mongoc_client_t`. +* ``callback``: A :symbol:`mongoc_oidc_callback_t`. + +Returns +------- + +Returns true on success. Returns false and logs on error. + + +.. seealso:: + | :doc:`mongoc_client_pool_set_oidc_callback` for setting a callback on a pooled client. + | :doc:`mongoc_oidc_callback_t` + | :doc:`mongoc_oidc_callback_params_t` diff --git a/src/libmongoc/doc/mongoc_client_t.rst b/src/libmongoc/doc/mongoc_client_t.rst index babe31f2437..2b8bdc40093 100644 --- a/src/libmongoc/doc/mongoc_client_t.rst +++ b/src/libmongoc/doc/mongoc_client_t.rst @@ -83,6 +83,7 @@ Example mongoc_client_set_apm_callbacks mongoc_client_set_appname mongoc_client_set_error_api + mongoc_client_set_oidc_callback mongoc_client_set_read_concern mongoc_client_set_read_prefs mongoc_client_set_server_api diff --git a/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst b/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst index 61de9cdb246..08f87d8d583 100644 --- a/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst +++ b/src/libmongoc/doc/mongoc_oidc_callback_params_get_timeout.rst @@ -13,7 +13,8 @@ Synopsis Return a value comparable with :symbol:`bson_get_monotonic_time()` to determine when a timeout must occur. -The return value is an absolute time point, not a duration. A callback can signal a timeout error using. Example: +The return value is an absolute time point, not a duration. A callback can signal a timeout error using +:symbol:`mongoc_oidc_callback_params_cancel_with_timeout`. Example: .. code-block:: c @@ -26,7 +27,7 @@ The return value is an absolute time point, not a duration. A callback can signa return mongoc_oidc_callback_params_cancel_with_timeout (params); } - // ... + // ... your code here ... } A ``NULL`` (unset) return value means "infinite" timeout. diff --git a/src/libmongoc/doc/mongoc_oidc_callback_params_t.rst b/src/libmongoc/doc/mongoc_oidc_callback_params_t.rst index d76c5c95008..51c0b90555c 100644 --- a/src/libmongoc/doc/mongoc_oidc_callback_params_t.rst +++ b/src/libmongoc/doc/mongoc_oidc_callback_params_t.rst @@ -125,7 +125,7 @@ This parameter must be set in advance via :symbol:`mongoc_oidc_callback_set_user user_data_t *user_data = malloc (sizeof (*user_data)); *user_data = (user_data_t){.counter = 0, .error_message = NULL}; mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new_with_user_data (&example_callback_fn, (void *) user_data); - mongoc_client_set_oidc_callback (client, callback); + BSON_ASSERT (mongoc_client_set_oidc_callback (client, callback)); mongoc_oidc_callback_destroy (callback); } diff --git a/src/libmongoc/doc/mongoc_oidc_callback_t.rst b/src/libmongoc/doc/mongoc_oidc_callback_t.rst index c3c4baf54b2..aa61fd1913a 100644 --- a/src/libmongoc/doc/mongoc_oidc_callback_t.rst +++ b/src/libmongoc/doc/mongoc_oidc_callback_t.rst @@ -57,7 +57,7 @@ The callback function stored by a :symbol:`mongoc_oidc_callback_t` object will b { mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new (&single_thread_only); - mongoc_client_set_oidc_callback (client, callback); + BSON_ASSERT (mongoc_client_set_oidc_callback (client, callback)); mongoc_oidc_callback_destroy (callback); } @@ -73,7 +73,7 @@ The callback function stored by a :symbol:`mongoc_oidc_callback_t` object will b { mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new (&single_thread_only); - mongoc_client_pool_set_oidc_callback (pool, callback); + BSON_ASSERT (mongoc_client_pool_set_oidc_callback (pool, callback)); mongoc_oidc_callback_destroy (callback); } @@ -102,8 +102,8 @@ If the callback is associated with more than one :symbol:`mongoc_client_t` objec { mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new (&many_threads_possible); - mongoc_client_set_oidc_callback (client_a, callback); - mongoc_client_set_oidc_callback (client_b, callback); + BSON_ASSERT (mongoc_client_set_oidc_callback (client_a, callback)); + BSON_ASSERT (mongoc_client_set_oidc_callback (client_b, callback)); mongoc_oidc_callback_destroy (callback); } @@ -130,8 +130,8 @@ If the callback is associated with more than one :symbol:`mongoc_client_t` objec { mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new (&many_threads_possible); - mongoc_client_pool_set_oidc_callback (pool_a, callback); - mongoc_client_pool_set_oidc_callback (pool_b, callback); + BSON_ASSERT (mongoc_client_pool_set_oidc_callback (pool_a, callback)); + BSON_ASSERT (mongoc_client_pool_set_oidc_callback (pool_b, callback)); mongoc_oidc_callback_destroy (callback); } diff --git a/src/libmongoc/src/mongoc/mongoc-client-pool.c b/src/libmongoc/src/mongoc/mongoc-client-pool.c index 275747b6a73..ff859e574b6 100644 --- a/src/libmongoc/src/mongoc/mongoc-client-pool.c +++ b/src/libmongoc/src/mongoc/mongoc-client-pool.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -689,3 +690,23 @@ mongoc_client_pool_set_server_api(mongoc_client_pool_t *pool, const mongoc_serve return true; } + +bool +mongoc_client_pool_set_oidc_callback(mongoc_client_pool_t *pool, const mongoc_oidc_callback_t *callback) +{ + BSON_ASSERT_PARAM(pool); + BSON_ASSERT_PARAM(callback); + + if (mongoc_oidc_cache_get_callback(pool->topology->oidc_cache)) { + MONGOC_ERROR("mongoc_client_pool_set_oidc_callback can only be called once per pool"); + return false; + } + + if (pool->client_initialized) { + MONGOC_ERROR("mongoc_client_pool_set_oidc_callback can only be called before mongoc_client_pool_pop"); + return false; + } + + mongoc_oidc_cache_set_callback(pool->topology->oidc_cache, callback); + return true; +} diff --git a/src/libmongoc/src/mongoc/mongoc-client-pool.h b/src/libmongoc/src/mongoc/mongoc-client-pool.h index 2cccd458753..16e385aba14 100644 --- a/src/libmongoc/src/mongoc/mongoc-client-pool.h +++ b/src/libmongoc/src/mongoc/mongoc-client-pool.h @@ -84,6 +84,9 @@ mongoc_client_pool_set_server_api(mongoc_client_pool_t *pool, const mongoc_serve MONGOC_EXPORT(bool) mongoc_client_pool_set_structured_log_opts(mongoc_client_pool_t *pool, const mongoc_structured_log_opts_t *opts); +MONGOC_EXPORT(bool) +mongoc_client_pool_set_oidc_callback(mongoc_client_pool_t *pool, const mongoc_oidc_callback_t *callback); + BSON_END_DECLS diff --git a/src/libmongoc/src/mongoc/mongoc-client.c b/src/libmongoc/src/mongoc/mongoc-client.c index 1cd342f0ec2..4e8d94f9529 100644 --- a/src/libmongoc/src/mongoc/mongoc-client.c +++ b/src/libmongoc/src/mongoc/mongoc-client.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include #include @@ -2723,3 +2724,24 @@ mongoc_client_uses_loadbalanced(const mongoc_client_t *client) return mongoc_topology_uses_loadbalanced(client->topology); } + +bool +mongoc_client_set_oidc_callback(mongoc_client_t *client, const mongoc_oidc_callback_t *callback) +{ + BSON_ASSERT_PARAM(client); + BSON_ASSERT_PARAM(callback); + + if (mongoc_oidc_cache_get_callback(client->topology->oidc_cache)) { + MONGOC_ERROR("mongoc_client_set_oidc_callback can only be called once per client"); + return false; + } + + if (!client->topology->single_threaded) { + MONGOC_ERROR("mongoc_client_set_oidc_callback must only be used for single threaded clients. " + "For client pools, use mongoc_client_pool_set_oidc_callback instead."); + return false; + } + + mongoc_oidc_cache_set_callback(client->topology->oidc_cache, callback); + return true; +} diff --git a/src/libmongoc/src/mongoc/mongoc-client.h b/src/libmongoc/src/mongoc/mongoc-client.h index a70e7a38fee..2b53c91b8b0 100644 --- a/src/libmongoc/src/mongoc/mongoc-client.h +++ b/src/libmongoc/src/mongoc/mongoc-client.h @@ -34,6 +34,7 @@ #ifdef MONGOC_ENABLE_SSL #include #endif +#include #include #include #include @@ -276,6 +277,9 @@ MONGOC_EXPORT(mongoc_server_description_t *) mongoc_client_get_handshake_description(mongoc_client_t *client, uint32_t server_id, bson_t *opts, bson_error_t *error) BSON_GNUC_WARN_UNUSED_RESULT; +MONGOC_EXPORT(bool) +mongoc_client_set_oidc_callback(mongoc_client_t *client, const mongoc_oidc_callback_t *callback); + BSON_END_DECLS diff --git a/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h b/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h new file mode 100644 index 00000000000..5839d85387f --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h @@ -0,0 +1,48 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef MONGOC_CLUSTER_OIDC_PRIVATE_H +#define MONGOC_CLUSTER_OIDC_PRIVATE_H + +#include +#include +#include + +struct _mongoc_cluster_t; // Forward declare. + +#include + +// mongoc_oidc_append_speculative_auth adds speculative auth. +bool +mongoc_oidc_append_speculative_auth(const char *access_token, uint32_t server_id, bson_t *cmd, bson_error_t *error); + + +bool +_mongoc_cluster_auth_node_oidc(struct _mongoc_cluster_t *cluster, + mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *conn_cache, + const mongoc_server_description_t *sd, + bson_error_t *error); + +bool +_mongoc_cluster_reauth_node_oidc(struct _mongoc_cluster_t *cluster, + mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *conn_cache, + const mongoc_server_description_t *sd, + bson_error_t *error); + +#endif /* MONGOC_CLUSTER_OIDC_PRIVATE_H */ diff --git a/src/libmongoc/src/mongoc/mongoc-cluster-oidc.c b/src/libmongoc/src/mongoc/mongoc-cluster-oidc.c new file mode 100644 index 00000000000..62c49489f83 --- /dev/null +++ b/src/libmongoc/src/mongoc/mongoc-cluster-oidc.c @@ -0,0 +1,216 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include +#include +#include + +#define SET_ERROR(...) _mongoc_set_error(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, __VA_ARGS__) + +bool +mongoc_oidc_append_speculative_auth(const char *access_token, uint32_t server_id, bson_t *cmd, bson_error_t *error) +{ + BSON_ASSERT_PARAM(access_token); + BSON_ASSERT_PARAM(cmd); + BSON_OPTIONAL_PARAM(error); + + bool ok = false; + + // Build `saslStart` command: + { + bsonBuildDecl(jwt_doc, kv("jwt", cstr(access_token))); + if (bsonBuildError) { + SET_ERROR("BSON error: %s", bsonBuildError); + goto fail; + } + + bson_init(cmd); + bsonBuild(*cmd, + kv("saslStart", int32(1)), + kv("mechanism", cstr("MONGODB-OIDC")), + kv("payload", binary(BSON_SUBTYPE_BINARY, bson_get_data(&jwt_doc), jwt_doc.len)), + kv("db", cstr("$external"))); + + if (bsonBuildError) { + SET_ERROR("BSON error: %s", bsonBuildError); + bson_destroy(&jwt_doc); + goto fail; + } + + bson_destroy(&jwt_doc); + } + + ok = true; +fail: + return ok; +} + +static bool +run_sasl_start(mongoc_cluster_t *cluster, + mongoc_stream_t *stream, + const mongoc_server_description_t *sd, + const char *access_token, + bson_error_t *error) +{ + BSON_ASSERT_PARAM(cluster); + BSON_ASSERT_PARAM(stream); + BSON_ASSERT_PARAM(sd); + BSON_ASSERT_PARAM(access_token); + BSON_OPTIONAL_PARAM(error); + + mongoc_server_stream_t *server_stream = NULL; + bson_t cmd = BSON_INITIALIZER; + bson_t reply = BSON_INITIALIZER; + bool ok = false; + + // Build `saslStart` command: + { + bsonBuildDecl(jwt_doc, kv("jwt", cstr(access_token))); + if (bsonBuildError) { + SET_ERROR("BSON error: %s", bsonBuildError); + goto fail; + } + + bsonBuild(cmd, + kv("saslStart", int32(1)), + kv("mechanism", cstr("MONGODB-OIDC")), + kv("payload", binary(BSON_SUBTYPE_BINARY, bson_get_data(&jwt_doc), jwt_doc.len))); + + if (bsonBuildError) { + SET_ERROR("BSON error: %s", bsonBuildError); + bson_destroy(&jwt_doc); + goto fail; + } + + bson_destroy(&jwt_doc); + } + + // Send command: + { + mongoc_cmd_parts_t parts; + + mc_shared_tpld td = mc_tpld_take_ref(BSON_ASSERT_PTR_INLINE(cluster)->client->topology); + + mongoc_cmd_parts_init(&parts, cluster->client, "$external", MONGOC_QUERY_NONE /* unused for OP_MSG */, &cmd); + parts.prohibit_lsid = true; // Do not append session ids to auth commands per session spec. + server_stream = _mongoc_cluster_create_server_stream(td.ptr, sd, stream); + mc_tpld_drop_ref(&td); + if (!mongoc_cluster_run_command_parts(cluster, server_stream, &parts, &reply, error)) { + goto fail; + } + } + + // Expect successful reply to include `done: true`: + { + bsonParse(reply, require(allOf(key("done"), isTrue), nop)); + if (bsonParseError) { + SET_ERROR("Error in OIDC reply: %s", bsonParseError); + goto fail; + } + } + + ok = true; + +fail: + bson_destroy(&reply); + mongoc_server_stream_cleanup(server_stream); + bson_destroy(&cmd); + return ok; +} + +bool +_mongoc_cluster_auth_node_oidc(mongoc_cluster_t *cluster, + mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *conn_cache, + const mongoc_server_description_t *sd, + bson_error_t *error) +{ + BSON_ASSERT_PARAM(cluster); + BSON_ASSERT_PARAM(stream); + BSON_ASSERT_PARAM(sd); + BSON_ASSERT_PARAM(error); + + bool ok = false; + char *access_token = NULL; + + // From spec: "If both ENVIRONMENT and an OIDC Callback [...] are provided the driver MUST raise an error." + bson_t authMechanismProperties = BSON_INITIALIZER; + mongoc_uri_get_mechanism_properties(cluster->client->uri, &authMechanismProperties); + if (mongoc_oidc_cache_get_callback(cluster->client->topology->oidc_cache) && + bson_has_field(&authMechanismProperties, "ENVIRONMENT")) { + SET_ERROR("MONGODB-OIDC requested with both ENVIRONMENT and an OIDC Callback. Use one or the other."); + goto done; + } + + + bool is_cache = false; + access_token = mongoc_oidc_cache_get_token(cluster->client->topology->oidc_cache, &is_cache, error); + if (!access_token) { + goto done; + } + + if (is_cache) { + mongoc_oidc_connection_cache_set(conn_cache, access_token); + if (!run_sasl_start(cluster, stream, sd, access_token, error)) { + if (error->code != MONGOC_SERVER_ERR_AUTHENTICATION) { + goto done; + } + // Retry getting the access token once: + mongoc_oidc_cache_invalidate_token(cluster->client->topology->oidc_cache, access_token); + bson_free(access_token); + access_token = mongoc_oidc_cache_get_token(cluster->client->topology->oidc_cache, &is_cache, error); + } else { + ok = true; + goto done; + } + } + + if (!access_token) { + goto done; + } + mongoc_oidc_connection_cache_set(conn_cache, access_token); + if (!run_sasl_start(cluster, stream, sd, access_token, error)) { + goto done; + } + + ok = true; +done: + bson_free(access_token); + return ok; +} + +bool +_mongoc_cluster_reauth_node_oidc(mongoc_cluster_t *cluster, + mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *oidc_connection_cache, + const mongoc_server_description_t *sd, + bson_error_t *error) +{ + char *connection_cached_token = mongoc_oidc_connection_cache_get(oidc_connection_cache); + if (connection_cached_token) { + // Invalidate shared cache: + mongoc_oidc_cache_invalidate_token(cluster->client->topology->oidc_cache, + connection_cached_token); // Does nothing if token was already invalidated. + // Clear connection cached: + mongoc_oidc_connection_cache_set(oidc_connection_cache, NULL); + } + bson_free(connection_cached_token); + return _mongoc_cluster_auth_node_oidc(cluster, stream, oidc_connection_cache, sd, error); +} diff --git a/src/libmongoc/src/mongoc/mongoc-cluster-private.h b/src/libmongoc/src/mongoc/mongoc-cluster-private.h index c043aacb33c..09c1464202b 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster-private.h +++ b/src/libmongoc/src/mongoc/mongoc-cluster-private.h @@ -50,6 +50,7 @@ typedef struct _mongoc_cluster_node_t { /* handshake_sd is a server description created from the handshake on the * stream. */ mongoc_server_description_t *handshake_sd; + mongoc_oidc_connection_cache_t *oidc_connection_cache; } mongoc_cluster_node_t; typedef struct _mongoc_cluster_t { @@ -82,6 +83,7 @@ mongoc_cluster_reset_sockettimeoutms(mongoc_cluster_t *cluster); void mongoc_cluster_disconnect_node(mongoc_cluster_t *cluster, uint32_t id); + int32_t mongoc_cluster_get_max_bson_obj_size(mongoc_cluster_t *cluster); diff --git a/src/libmongoc/src/mongoc/mongoc-cluster.c b/src/libmongoc/src/mongoc/mongoc-cluster.c index bd842ddeca4..f433a3377e1 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster.c +++ b/src/libmongoc/src/mongoc/mongoc-cluster.c @@ -40,6 +40,7 @@ #include #include #include +#include #include #include #include @@ -465,26 +466,9 @@ _handle_txn_error_labels(bool cmd_ret, const bson_error_t *cmd_err, const mongoc _mongoc_write_error_handle_labels(cmd_ret, cmd_err, reply, cmd->server_stream->sd); } -/* - *-------------------------------------------------------------------------- - * - * mongoc_cluster_run_command_monitored -- - * - * Internal function to run a command on a given stream. - * @error and @reply are optional out-pointers. - * - * Returns: - * true if successful; otherwise false and @error is set. - * - * Side effects: - * If the client's APM callbacks are set, they are executed. - * @reply is set and should ALWAYS be released with bson_destroy(). - * - *-------------------------------------------------------------------------- - */ - -bool -mongoc_cluster_run_command_monitored(mongoc_cluster_t *cluster, mongoc_cmd_t *cmd, bson_t *reply, bson_error_t *error) +// run_command_monitored is an internal helper to run a command with APM monitoring. +static bool +run_command_monitored(mongoc_cluster_t *cluster, mongoc_cmd_t *cmd, bson_t *reply, bson_error_t *error) { bool retval; const int32_t request_id = ++cluster->request_id; @@ -659,6 +643,90 @@ mongoc_cluster_run_command_monitored(mongoc_cluster_t *cluster, mongoc_cmd_t *cm return retval; } +// _try_get_oidc_connection_cache returns the OIDC connection cache (for either a single or pooled client). +// Returns NULL if server is not found. +static mongoc_oidc_connection_cache_t * +_try_get_oidc_connection_cache(mongoc_cluster_t *cluster, uint32_t server_id, bson_error_t *error) +{ + mongoc_oidc_connection_cache_t *ret = NULL; + + if (cluster->client->topology->single_threaded) { + mongoc_topology_scanner_node_t *scanner_node = + mongoc_topology_scanner_get_node(cluster->client->topology->scanner, server_id); + if (scanner_node) { + ret = scanner_node->oidc_connection_cache; + } + } else { + mongoc_cluster_node_t *cluster_node = (mongoc_cluster_node_t *)mongoc_set_get(cluster->nodes, server_id); + if (cluster_node) { + ret = cluster_node->oidc_connection_cache; + } + } + + if (!ret) { + // Possible on bad server_id or if server was removed. Unexpected with current call paths. + _mongoc_set_error(error, + MONGOC_ERROR_COMMAND, + MONGOC_ERROR_COMMAND_INVALID_ARG, + "Could not find server with id: %" PRIu32, + server_id); + } + return ret; +} + +/* + *-------------------------------------------------------------------------- + * + * mongoc_cluster_run_command_monitored -- + * + * Internal function to run a command on a given stream. + * @error and @reply are optional out-pointers. + * If the server returns a ReauthenticationRequired error, auth + * may be re-attempted. + * + * Returns: + * true if successful; otherwise false and @error is set. + * + * Side effects: + * If the client's APM callbacks are set, they are executed. + * @reply is set and should ALWAYS be released with bson_destroy(). + * + *-------------------------------------------------------------------------- + */ + +bool +mongoc_cluster_run_command_monitored(mongoc_cluster_t *cluster, mongoc_cmd_t *cmd, bson_t *reply, bson_error_t *error) +{ + bool ok = run_command_monitored(cluster, cmd, reply, error); + if (!ok) { + const char *mechanism = mongoc_uri_get_auth_mechanism(cluster->uri); + bool using_oidc = mechanism && 0 == strcasecmp(mechanism, "MONGODB-OIDC"); + + // From auth spec: + // > If any operation fails with `ReauthenticationRequired` (error code 391) and MONGODB-OIDC is in use, the + // > driver MUST reauthenticate the connection. + if (using_oidc && _mongoc_error_is_reauth(error, cluster->client->error_api_version)) { + if (reply) { + bson_destroy(reply); + bson_init(reply); + } + + mongoc_oidc_connection_cache_t *oidc_connection_cache = + _try_get_oidc_connection_cache(cluster, cmd->server_stream->sd->id, error); + if (!oidc_connection_cache) { + return false; + } + + if (!_mongoc_cluster_reauth_node_oidc( + cluster, cmd->server_stream->stream, oidc_connection_cache, cmd->server_stream->sd, error)) { + return false; + } + return run_command_monitored(cluster, cmd, reply, error); + } + } + return ok; +} + static bool _should_use_op_msg(const mongoc_cluster_t *cluster) @@ -783,6 +851,7 @@ mongoc_cluster_run_command_parts(mongoc_cluster_t *cluster, static mongoc_server_description_t * _stream_run_hello(mongoc_cluster_t *cluster, mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *oidc_connection_cache, const char *address, uint32_t server_id, bool negotiate_sasl_supported_mechs, @@ -800,7 +869,11 @@ _stream_run_hello(mongoc_cluster_t *cluster, _mongoc_topology_dup_handshake_cmd(cluster->client->topology, &handshake_command); if (cluster->requires_auth && speculative_auth_response) { - _mongoc_topology_scanner_add_speculative_authentication(&handshake_command, cluster->uri, scram); + char *oidc_access_token = mongoc_oidc_cache_get_cached_token(cluster->client->topology->oidc_cache); + _mongoc_topology_scanner_add_speculative_authentication( + &handshake_command, cluster->uri, oidc_access_token, server_id, scram); + mongoc_oidc_connection_cache_set(oidc_connection_cache, oidc_access_token); + bson_free(oidc_access_token); } if (negotiate_sasl_supported_mechs) { @@ -935,6 +1008,7 @@ _cluster_run_hello(mongoc_cluster_t *cluster, sd = _stream_run_hello(cluster, node->stream, + node->oidc_connection_cache, node->connection_address, server_id, _mongoc_uri_requires_auth_negotiation(cluster->uri), @@ -1583,6 +1657,7 @@ _mongoc_cluster_auth_node_scram_sha_256(mongoc_cluster_t *cluster, static bool _mongoc_cluster_auth_node(mongoc_cluster_t *cluster, mongoc_stream_t *stream, + mongoc_oidc_connection_cache_t *oidc_connection_cache, mongoc_server_description_t *sd, const mongoc_handshake_sasl_supported_mechs_t *sasl_supported_mechs, bson_error_t *error) @@ -1623,6 +1698,8 @@ _mongoc_cluster_auth_node(mongoc_cluster_t *cluster, ret = _mongoc_cluster_auth_node_plain(cluster, stream, sd, error); } else if (0 == strcasecmp(mechanism, "MONGODB-AWS")) { ret = _mongoc_cluster_auth_node_aws(cluster, stream, sd, error); + } else if (0 == strcasecmp(mechanism, "MONGODB-OIDC")) { + ret = _mongoc_cluster_auth_node_oidc(cluster, stream, oidc_connection_cache, sd, error); } else { _mongoc_set_error(error, MONGOC_ERROR_CLIENT, @@ -1683,6 +1760,7 @@ _mongoc_cluster_node_destroy(mongoc_cluster_node_t *node) mongoc_stream_failed(node->stream); bson_free(node->connection_address); mongoc_server_description_destroy(node->handshake_sd); + mongoc_oidc_connection_cache_destroy(node->oidc_connection_cache); bson_free(node); } @@ -1763,6 +1841,22 @@ _mongoc_cluster_finish_speculative_auth(mongoc_cluster_t *cluster, } #endif + if (strcasecmp(mechanism, "MONGODB-OIDC") == 0) { + // Expect successful reply to include `done: true`: + { + auth_handled = true; + + bsonParse(*speculative_auth_response, require(allOf(key("done"), isTrue), nop)); + if (bsonParseError) { + _mongoc_set_error( + error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Error in OIDC reply: %s", bsonParseError); + ret = false; + } else { + ret = true; + } + } + } + if (auth_handled) { if (!ret) { mongoc_counter_auth_failure_inc(); @@ -1833,6 +1927,7 @@ _cluster_add_node(mongoc_cluster_t *cluster, /* take critical fields from a fresh hello */ cluster_node = _mongoc_cluster_node_new(stream, host->host_and_port); + cluster_node->oidc_connection_cache = mongoc_oidc_connection_cache_new(); handshake_sd = _cluster_run_hello(cluster, cluster_node, server_id, &scram, &speculative_auth_response, error); if (!handshake_sd) { @@ -1846,8 +1941,12 @@ _cluster_add_node(mongoc_cluster_t *cluster, bool is_auth = _mongoc_cluster_finish_speculative_auth( cluster, stream, handshake_sd, &speculative_auth_response, &scram, error); - if (!is_auth && - !_mongoc_cluster_auth_node(cluster, cluster_node->stream, handshake_sd, &sasl_supported_mechs, error)) { + if (!is_auth && !_mongoc_cluster_auth_node(cluster, + cluster_node->stream, + cluster_node->oidc_connection_cache, + handshake_sd, + &sasl_supported_mechs, + error)) { MONGOC_WARNING("Failed authentication to %s (%s)", host->host_and_port, error->message); mongoc_server_description_destroy(handshake_sd); GOTO(error); @@ -2173,9 +2272,12 @@ _cluster_fetch_stream_single(mongoc_cluster_t *cluster, return NULL; } - if (!has_speculative_auth && - !_mongoc_cluster_auth_node( - cluster, scanner_node->stream, handshake_sd, &scanner_node->sasl_supported_mechs, &handshake_sd->error)) { + if (!has_speculative_auth && !_mongoc_cluster_auth_node(cluster, + scanner_node->stream, + scanner_node->oidc_connection_cache, + handshake_sd, + &scanner_node->sasl_supported_mechs, + &handshake_sd->error)) { *error = handshake_sd->error; mongoc_server_description_destroy(handshake_sd); return NULL; diff --git a/src/libmongoc/src/mongoc/mongoc-error-private.h b/src/libmongoc/src/mongoc/mongoc-error-private.h index c22380f8c5c..b841ae526ee 100644 --- a/src/libmongoc/src/mongoc/mongoc-error-private.h +++ b/src/libmongoc/src/mongoc/mongoc-error-private.h @@ -55,7 +55,9 @@ typedef enum { MONGOC_SERVER_ERR_NOTPRIMARYNOSECONDARYOK = 13435, MONGOC_SERVER_ERR_NOTPRIMARYORSECONDARY = 13436, MONGOC_SERVER_ERR_LEGACYNOTPRIMARY = 10058, - MONGOC_SERVER_ERR_NS_NOT_FOUND = 26 + MONGOC_SERVER_ERR_NS_NOT_FOUND = 26, + MONGOC_SERVER_ERR_AUTHENTICATION = 18, + MONGOC_SERVER_ERR_REAUTHENTICATION_REQUIRED = 391, } mongoc_server_err_t; mongoc_read_err_type_t @@ -94,6 +96,9 @@ _mongoc_error_is_server(const bson_error_t *error); bool _mongoc_error_is_auth(const bson_error_t *error); +bool +_mongoc_error_is_reauth(const bson_error_t *error, int error_api_version); + /* Try to append `s` to `error`. Truncates `s` if `error` is out of space. */ void _mongoc_error_append(bson_error_t *error, const char *s); diff --git a/src/libmongoc/src/mongoc/mongoc-error.c b/src/libmongoc/src/mongoc/mongoc-error.c index 666a16b68f1..62b13f6cede 100644 --- a/src/libmongoc/src/mongoc/mongoc-error.c +++ b/src/libmongoc/src/mongoc/mongoc-error.c @@ -313,6 +313,18 @@ _mongoc_error_is_auth(const bson_error_t *error) return error->domain == MONGOC_ERROR_CLIENT && error->code == MONGOC_ERROR_CLIENT_AUTHENTICATE; } +bool +_mongoc_error_is_reauth(const bson_error_t *error, int error_api_version) +{ + if (!error) { + return false; + } + + uint32_t expected_domain = + error_api_version == MONGOC_ERROR_API_VERSION_2 ? MONGOC_ERROR_SERVER : MONGOC_ERROR_QUERY; + return error->domain == expected_domain && error->code == MONGOC_SERVER_ERR_REAUTHENTICATION_REQUIRED; +} + void _mongoc_error_append(bson_error_t *error, const char *s) { diff --git a/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h b/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h index 3c6f90f47b5..0f3ae5745e2 100644 --- a/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h +++ b/src/libmongoc/src/mongoc/mongoc-oidc-cache-private.h @@ -63,4 +63,22 @@ mongoc_oidc_cache_invalidate_token(mongoc_oidc_cache_t *cache, const char *token void mongoc_oidc_cache_destroy(mongoc_oidc_cache_t *); +// mongoc_oidc_connection_cache_t implements the OIDC spec "Connection Cache". +// Stores a possible OIDC access token used to authenticate one mongoc_stream_t. +typedef struct mongoc_oidc_connection_cache_t mongoc_oidc_connection_cache_t; + +mongoc_oidc_connection_cache_t * +mongoc_oidc_connection_cache_new(void); + +// mongoc_oidc_connection_cache_set overwrites the cached token. Pass a NULL token to clear cache. +void +mongoc_oidc_connection_cache_set(mongoc_oidc_connection_cache_t *cache, const char *token); + +// mongoc_oidc_connection_cache_get returns the cached token or NULL. +char * +mongoc_oidc_connection_cache_get(const mongoc_oidc_connection_cache_t *cache); + +void +mongoc_oidc_connection_cache_destroy(mongoc_oidc_connection_cache_t *cache); + #endif // MONGOC_OIDC_CACHE_PRIVATE_H diff --git a/src/libmongoc/src/mongoc/mongoc-oidc-cache.c b/src/libmongoc/src/mongoc/mongoc-oidc-cache.c index d32e04c96ba..96ece4e2646 100644 --- a/src/libmongoc/src/mongoc/mongoc-oidc-cache.c +++ b/src/libmongoc/src/mongoc/mongoc-oidc-cache.c @@ -227,3 +227,40 @@ mongoc_oidc_cache_invalidate_token(mongoc_oidc_cache_t *cache, const char *token bson_free(old_token); } + +struct mongoc_oidc_connection_cache_t { + char *token; +}; + +mongoc_oidc_connection_cache_t * +mongoc_oidc_connection_cache_new(void) +{ + mongoc_oidc_connection_cache_t *oidc = bson_malloc0(sizeof(mongoc_oidc_connection_cache_t)); + return oidc; +} + +void +mongoc_oidc_connection_cache_set(mongoc_oidc_connection_cache_t *cache, const char *token) +{ + BSON_ASSERT_PARAM(cache); + BSON_OPTIONAL_PARAM(token); + bson_free(cache->token); + cache->token = bson_strdup(token); +} + +char * +mongoc_oidc_connection_cache_get(const mongoc_oidc_connection_cache_t *cache) +{ + BSON_ASSERT_PARAM(cache); + return bson_strdup(cache->token); +} + +void +mongoc_oidc_connection_cache_destroy(mongoc_oidc_connection_cache_t *cache) +{ + if (!cache) { + return; + } + bson_free(cache->token); + bson_free(cache); +} diff --git a/src/libmongoc/src/mongoc/mongoc-topology-private.h b/src/libmongoc/src/mongoc/mongoc-topology-private.h index 7b9e579a893..2a8051ff6c7 100644 --- a/src/libmongoc/src/mongoc/mongoc-topology-private.h +++ b/src/libmongoc/src/mongoc/mongoc-topology-private.h @@ -21,9 +21,11 @@ #include #include +#include #include #include #include +#include #include #include #include @@ -32,6 +34,7 @@ #include #include +#include #include #include @@ -233,6 +236,9 @@ typedef struct _mongoc_topology_t { // DNS implementations are expected to try UDP first, then retry with TCP if the UDP response indicates truncation. // Some DNS servers truncate UDP responses without setting the truncated (TC) flag. This may result in no TCP retry. bool srv_prefer_tcp; + + // `oidc_cache` implements the OIDC spec "Client Cache". It is shared among all pooled clients. + mongoc_oidc_cache_t *oidc_cache; } mongoc_topology_t; mongoc_topology_t * diff --git a/src/libmongoc/src/mongoc/mongoc-topology-scanner-private.h b/src/libmongoc/src/mongoc/mongoc-topology-scanner-private.h index 990949e045e..1bc68c16e3a 100644 --- a/src/libmongoc/src/mongoc/mongoc-topology-scanner-private.h +++ b/src/libmongoc/src/mongoc/mongoc-topology-scanner-private.h @@ -52,6 +52,7 @@ typedef struct mongoc_topology_scanner_node { uint32_t id; /* after scanning, this is set to the successful stream if one exists. */ mongoc_stream_t *stream; + mongoc_oidc_connection_cache_t *oidc_connection_cache; int64_t last_used; /* last_failed is set upon a network error trying to check a server. @@ -148,6 +149,10 @@ typedef struct mongoc_topology_scanner { mongoc_server_api_t *api; mongoc_log_and_monitor_instance_t *log_and_monitor; // Not null. bool loadbalanced; + + // oidc_cache is used to create the OIDC speculative auth command. + mongoc_oidc_cache_t *oidc_cache; + } mongoc_topology_scanner_t; mongoc_topology_scanner_t * @@ -208,9 +213,8 @@ mongoc_topology_scanner_node_t * mongoc_topology_scanner_get_node(mongoc_topology_scanner_t *ts, uint32_t id); void -_mongoc_topology_scanner_add_speculative_authentication(bson_t *cmd, - const mongoc_uri_t *uri, - mongoc_scram_t *scram /* OUT */); +_mongoc_topology_scanner_add_speculative_authentication( + bson_t *cmd, const mongoc_uri_t *uri, char *oidc_access_token, uint32_t server_id, mongoc_scram_t *scram /* OUT */); void _mongoc_topology_scanner_parse_speculative_authentication(const bson_t *hello, bson_t *speculative_authenticate); @@ -271,6 +275,9 @@ mongoc_topology_scanner_uses_server_api(const mongoc_topology_scanner_t *ts); bool mongoc_topology_scanner_uses_loadbalanced(const mongoc_topology_scanner_t *ts); +void +_mongoc_topology_scanner_set_oidc_cache(mongoc_topology_scanner_t *ts, mongoc_oidc_cache_t *oidc_cache); + BSON_END_DECLS #endif /* MONGOC_TOPOLOGY_SCANNER_PRIVATE_H */ diff --git a/src/libmongoc/src/mongoc/mongoc-topology-scanner.c b/src/libmongoc/src/mongoc/mongoc-topology-scanner.c index 339223a1b46..46d357ced1e 100644 --- a/src/libmongoc/src/mongoc/mongoc-topology-scanner.c +++ b/src/libmongoc/src/mongoc/mongoc-topology-scanner.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -216,10 +217,14 @@ _mongoc_topology_scanner_get_speculative_auth_mechanism(const mongoc_uri_t *uri) } void -_mongoc_topology_scanner_add_speculative_authentication(bson_t *cmd, - const mongoc_uri_t *uri, - mongoc_scram_t *scram /* OUT */) +_mongoc_topology_scanner_add_speculative_authentication( + bson_t *cmd, const mongoc_uri_t *uri, char *oidc_access_token, uint32_t server_id, mongoc_scram_t *scram /* OUT */) { + BSON_ASSERT_PARAM(cmd); + BSON_ASSERT_PARAM(uri); + BSON_OPTIONAL_PARAM(oidc_access_token); + BSON_ASSERT_PARAM(scram); + bson_t auth_cmd; bson_error_t error; bool has_auth = false; @@ -259,6 +264,14 @@ _mongoc_topology_scanner_add_speculative_authentication(bson_t *cmd, } #endif + if (strcasecmp(mechanism, "MONGODB-OIDC") == 0 && oidc_access_token) { + if (mongoc_oidc_append_speculative_auth(oidc_access_token, server_id, &auth_cmd, &error)) { + has_auth = true; + } else { + MONGOC_ERROR("Error adding MONGODB-OIDC speculative auth: %s", error.message); + } + } + if (has_auth) { BSON_APPEND_DOCUMENT(cmd, "speculativeAuthenticate", &auth_cmd); bson_destroy(&auth_cmd); @@ -422,7 +435,10 @@ _begin_hello_cmd(mongoc_topology_scanner_node_t *node, if (node->ts->speculative_authentication && !node->has_auth && bson_empty(&node->speculative_auth_response) && node->scram.step == 0) { - _mongoc_topology_scanner_add_speculative_authentication(&cmd, ts->uri, &node->scram); + char *oidc_access_token = mongoc_oidc_cache_get_cached_token(ts->oidc_cache); + _mongoc_topology_scanner_add_speculative_authentication(&cmd, ts->uri, oidc_access_token, node->id, &node->scram); + mongoc_oidc_connection_cache_set(node->oidc_connection_cache, oidc_access_token); + bson_free(oidc_access_token); } if (!bson_empty(&ts->cluster_time)) { @@ -558,6 +574,7 @@ mongoc_topology_scanner_add(mongoc_topology_scanner_t *ts, const mongoc_host_lis node->last_failed = -1; node->last_used = -1; node->hello_ok = hello_ok; + node->oidc_connection_cache = mongoc_oidc_connection_cache_new(); bson_init(&node->speculative_auth_response); DL_APPEND(ts->nodes, node); @@ -616,6 +633,7 @@ mongoc_topology_scanner_node_disconnect(mongoc_topology_scanner_node_t *node, bo } mongoc_server_description_destroy(node->handshake_sd); node->handshake_sd = NULL; + mongoc_oidc_connection_cache_set(node->oidc_connection_cache, NULL); } void @@ -633,6 +651,8 @@ mongoc_topology_scanner_node_destroy(mongoc_topology_scanner_node_t *node, bool _mongoc_scram_destroy(&node->scram); #endif + mongoc_oidc_connection_cache_destroy(node->oidc_connection_cache); + bson_free(node); } @@ -1506,3 +1526,11 @@ mongoc_topology_scanner_uses_loadbalanced(const mongoc_topology_scanner_t *ts) BSON_ASSERT_PARAM(ts); return ts->loadbalanced; } + +void +_mongoc_topology_scanner_set_oidc_cache(mongoc_topology_scanner_t *ts, mongoc_oidc_cache_t *oidc_cache) +{ + BSON_ASSERT_PARAM(ts); + BSON_ASSERT_PARAM(oidc_cache); + ts->oidc_cache = oidc_cache; +} diff --git a/src/libmongoc/src/mongoc/mongoc-topology.c b/src/libmongoc/src/mongoc/mongoc-topology.c index d7c5043178d..397ef3787a0 100644 --- a/src/libmongoc/src/mongoc/mongoc-topology.c +++ b/src/libmongoc/src/mongoc/mongoc-topology.c @@ -390,6 +390,8 @@ mongoc_topology_new(const mongoc_uri_t *uri, bool single_threaded) #endif topology = (mongoc_topology_t *)bson_malloc0(sizeof *topology); + + topology->oidc_cache = mongoc_oidc_cache_new(); // Check if requested to use TCP for SRV lookup. { char *srv_prefer_tcp = _mongoc_getenv("MONGOC_EXPERIMENTAL_SRV_PREFER_TCP"); @@ -464,6 +466,7 @@ mongoc_topology_new(const mongoc_uri_t *uri, bool single_threaded) topology, topology->connect_timeout_msec); + _mongoc_topology_scanner_set_oidc_cache(topology->scanner, topology->oidc_cache); bson_mutex_init(&topology->tpld_modification_mtx); mongoc_cond_init(&topology->cond_client); @@ -715,6 +718,8 @@ mongoc_topology_destroy(mongoc_topology_t *topology) bson_destroy(topology->encrypted_fields_map); + mongoc_oidc_cache_destroy(topology->oidc_cache); + bson_free(topology); } diff --git a/src/libmongoc/src/mongoc/mongoc-util.c b/src/libmongoc/src/mongoc/mongoc-util.c index 1d59df986f6..ad6930b8158 100644 --- a/src/libmongoc/src/mongoc/mongoc-util.c +++ b/src/libmongoc/src/mongoc/mongoc-util.c @@ -110,6 +110,9 @@ mongoc_client_set_usleep_impl(mongoc_client_t *client, mongoc_usleep_func_t usle { client->topology->usleep_fn = usleep_func; client->topology->usleep_data = user_data; + if (client->topology->oidc_cache) { + mongoc_oidc_cache_set_usleep_fn(client->topology->oidc_cache, usleep_func, user_data); + } } void diff --git a/src/libmongoc/tests/json/auth/connection-string.json b/src/libmongoc/tests/json/auth/legacy/connection-string.json similarity index 100% rename from src/libmongoc/tests/json/auth/connection-string.json rename to src/libmongoc/tests/json/auth/legacy/connection-string.json diff --git a/src/libmongoc/tests/json/auth/unified/mongodb-oidc-no-retry.json b/src/libmongoc/tests/json/auth/unified/mongodb-oidc-no-retry.json new file mode 100644 index 00000000000..f88527e5cb8 --- /dev/null +++ b/src/libmongoc/tests/json/auth/unified/mongodb-oidc-no-retry.json @@ -0,0 +1,422 @@ +{ + "description": "MONGODB-OIDC authentication with retry disabled", + "schemaVersion": "1.19", + "runOnRequirements": [ + { + "minServerVersion": "7.0", + "auth": true, + "authMechanism": "MONGODB-OIDC", + "serverless": "forbid" + } + ], + "createEntities": [ + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "client0", + "uriOptions": { + "authMechanism": "MONGODB-OIDC", + "authMechanismProperties": { + "$$placeholder": 1 + }, + "retryReads": false, + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collName" + } + } + ], + "initialData": [ + { + "collectionName": "collName", + "databaseName": "test", + "documents": [] + } + ], + "tests": [ + { + "description": "A read operation should succeed", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "A write operation should succeed", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Read commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake with cached token should use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "isClientError": true + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 18 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake without cached token should not use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 18 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "errorCode": 18 + } + } + ] + } + ] +} diff --git a/src/libmongoc/tests/test-libmongoc-main.c b/src/libmongoc/tests/test-libmongoc-main.c index 5d1c319b7e7..8508ced2426 100644 --- a/src/libmongoc/tests/test-libmongoc-main.c +++ b/src/libmongoc/tests/test-libmongoc-main.c @@ -164,6 +164,7 @@ main(int argc, char *argv[]) TEST_INSTALL(test_mongoc_oidc_callback_install); TEST_INSTALL(test_secure_channel_install); TEST_INSTALL(test_stream_tracker_install); + TEST_INSTALL(test_oidc_auth_install); const int ret = TestSuite_Run(&suite); diff --git a/src/libmongoc/tests/test-libmongoc.c b/src/libmongoc/tests/test-libmongoc.c index 0c622efbc1b..54076af7927 100644 --- a/src/libmongoc/tests/test-libmongoc.c +++ b/src/libmongoc/tests/test-libmongoc.c @@ -2621,3 +2621,50 @@ skip_if_no_large_allocations(void) { return test_framework_getenv_bool("MONGOC_TEST_LARGE_ALLOCATIONS"); } + +bool +test_framework_is_oidc(void) +{ + return test_framework_getenv_bool("MONGOC_TEST_OIDC"); +} + +static char * +read_test_token(void) +{ + FILE *token_file = fopen("/tmp/tokens/test_machine", "r"); + ASSERT(token_file); + + // Determine length of token: + ASSERT(0 == fseek(token_file, 0, SEEK_END)); + long token_len = ftell(token_file); + ASSERT(token_len > 0); + ASSERT(0 == fseek(token_file, 0, SEEK_SET)); + + // Read file into buffer: + char *token = bson_malloc(token_len + 1); + size_t nread = fread(token, 1, token_len, token_file); + ASSERT(nread == (size_t)token_len); + token[token_len] = '\0'; + fclose(token_file); + return token; +} + +static mongoc_oidc_credential_t * +oidc_callback_fn(mongoc_oidc_callback_params_t *params) +{ + char *token = read_test_token(); + mongoc_oidc_credential_t *cred = mongoc_oidc_credential_new(token); + bson_free(token); + return cred; +} + +void +test_framework_set_oidc_callback(mongoc_client_t *client) +{ + if (!test_framework_is_oidc()) { + return; + } + mongoc_oidc_callback_t *callback = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_client_set_oidc_callback(client, callback); + mongoc_oidc_callback_destroy(callback); +} diff --git a/src/libmongoc/tests/test-libmongoc.h b/src/libmongoc/tests/test-libmongoc.h index e3d05d2db06..3b54f9d7f57 100644 --- a/src/libmongoc/tests/test-libmongoc.h +++ b/src/libmongoc/tests/test-libmongoc.h @@ -274,6 +274,12 @@ test_framework_skip_if_no_getlasterror(void); int test_framework_skip_if_no_exhaust_cursors(void); +bool +test_framework_is_oidc(void); + +void +test_framework_set_oidc_callback(mongoc_client_t *client); + bool test_framework_is_loadbalanced(void); diff --git a/src/libmongoc/tests/test-mongoc-connection-uri.c b/src/libmongoc/tests/test-mongoc-connection-uri.c index a3e70b658ae..bb8cac452ff 100644 --- a/src/libmongoc/tests/test-mongoc-connection-uri.c +++ b/src/libmongoc/tests/test-mongoc-connection-uri.c @@ -480,7 +480,7 @@ test_all_spec_tests(TestSuite *suite) { install_json_test_suite(suite, JSON_DIR, "uri-options", &test_connection_uri_cb); install_json_test_suite(suite, JSON_DIR, "connection_uri", &test_connection_uri_cb); - install_json_test_suite(suite, JSON_DIR, "auth", &test_connection_uri_cb); + install_json_test_suite(suite, JSON_DIR, "auth/legacy", &test_connection_uri_cb); } diff --git a/src/libmongoc/tests/test-mongoc-oidc-cache.c b/src/libmongoc/tests/test-mongoc-oidc-cache.c index 16294b0ff32..93df56f6a41 100644 --- a/src/libmongoc/tests/test-mongoc-oidc-cache.c +++ b/src/libmongoc/tests/test-mongoc-oidc-cache.c @@ -353,6 +353,31 @@ test_oidc_cache_invalidate(void) } +// test_oidc_connection_cache tests the connection token cache. +static void +test_oidc_connection_cache(void) +{ + mongoc_oidc_connection_cache_t *cache = mongoc_oidc_connection_cache_new(); + + ASSERT(!mongoc_oidc_connection_cache_get(cache)); + + // Can set a cached token: + { + mongoc_oidc_connection_cache_set(cache, PLACEHOLDER_TOKEN); + char *got = mongoc_oidc_connection_cache_get(cache); + ASSERT_CMPSTR(got, PLACEHOLDER_TOKEN); + bson_free(got); + } + + // Can clear cached token: + { + mongoc_oidc_connection_cache_set(cache, NULL); + ASSERT(!mongoc_oidc_connection_cache_get(cache)); + } + + mongoc_oidc_connection_cache_destroy(cache); +} + void test_mongoc_oidc_install(TestSuite *suite) { @@ -363,4 +388,5 @@ test_mongoc_oidc_install(TestSuite *suite) TestSuite_Add(suite, "/oidc/cache/propagates_error", test_oidc_cache_propagates_error); TestSuite_Add(suite, "/oidc/cache/invalidate", test_oidc_cache_invalidate); TestSuite_Add(suite, "/oidc/cache/waits_between_calls", test_oidc_cache_waits_between_calls); + TestSuite_Add(suite, "/oidc/connection_cache", test_oidc_connection_cache); } diff --git a/src/libmongoc/tests/test-mongoc-oidc.c b/src/libmongoc/tests/test-mongoc-oidc.c new file mode 100644 index 00000000000..4e3a487f48a --- /dev/null +++ b/src/libmongoc/tests/test-mongoc-oidc.c @@ -0,0 +1,866 @@ +/* + * Copyright 2009-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +static char * +read_test_token(void) +{ + FILE *token_file = fopen("/tmp/tokens/test_machine", "r"); + ASSERT(token_file); + + // Determine length of token: + ASSERT(0 == fseek(token_file, 0, SEEK_END)); + long token_len = ftell(token_file); + ASSERT(token_len > 0); + ASSERT(0 == fseek(token_file, 0, SEEK_SET)); + + // Read file into buffer: + char *token = bson_malloc(token_len + 1); + size_t nread = fread(token, 1, token_len, token_file); + ASSERT(nread == (size_t)token_len); + token[token_len] = '\0'; + fclose(token_file); + return token; +} + +typedef struct { + bool validate_params; + bool return_null; + bool return_bad_token; + bool return_bad_token_after_first_call; +} callback_config_t; + +typedef struct { + int call_count; + callback_config_t config; +} callback_ctx_t; + +static mongoc_oidc_credential_t * +oidc_callback_fn(mongoc_oidc_callback_params_t *params) +{ + callback_ctx_t *ctx = mongoc_oidc_callback_params_get_user_data(params); + ASSERT(ctx); + ctx->call_count += 1; + + if (ctx->config.return_null) { + return NULL; + } + + if (ctx->config.return_bad_token) { + return mongoc_oidc_credential_new("bad_token"); + } + + if (ctx->config.return_bad_token_after_first_call && ctx->call_count > 1) { + return mongoc_oidc_credential_new("bad_token"); + } + + if (ctx->config.validate_params) { + const int64_t *timeout = mongoc_oidc_callback_params_get_timeout(params); + ASSERT(timeout); + // Expect the timeout to be set to 60 seconds from the start. + ASSERT_CMPINT64(*timeout, >=, bson_get_monotonic_time()); + ASSERT_CMPINT64(*timeout, <=, bson_get_monotonic_time() + 60 * 1000 * 1000); + + int version = mongoc_oidc_callback_params_get_version(params); + ASSERT_CMPINT(version, ==, 1); + + const char *username = mongoc_oidc_callback_params_get_username(params); + ASSERT(!username); + } + + char *token = read_test_token(); + mongoc_oidc_credential_t *cred = mongoc_oidc_credential_new(token); + bson_free(token); + return cred; +} + + +typedef struct { + mongoc_client_pool_t *pool; // May be NULL. + mongoc_client_t *client; + callback_ctx_t ctx; +} test_fixture_t; + +typedef struct { + bool use_pool; + bool use_error_api_v1; + callback_config_t callback_config; +} test_config_t; + +static test_fixture_t * +test_fixture_new(test_config_t cfg) +{ + test_fixture_t *tf = bson_malloc0(sizeof(*tf)); + + mongoc_uri_t *uri = mongoc_uri_new("mongodb://localhost:27017"); // Direct connect for simpler op counters. + mongoc_uri_set_auth_mechanism(uri, "MONGODB-OIDC"); + mongoc_uri_set_option_as_bool(uri, MONGOC_URI_RETRYREADS, false); // Disable retryable reads per spec. + + mongoc_oidc_callback_t *oidc_callback = mongoc_oidc_callback_new(oidc_callback_fn); + tf->ctx.config = cfg.callback_config; + mongoc_oidc_callback_set_user_data(oidc_callback, &tf->ctx); + + if (cfg.use_pool) { + tf->pool = mongoc_client_pool_new(uri); + mongoc_client_pool_set_error_api(tf->pool, MONGOC_ERROR_API_VERSION_2); + ASSERT(mongoc_client_pool_set_oidc_callback(tf->pool, oidc_callback)); + tf->client = mongoc_client_pool_pop(tf->pool); + } else { + tf->client = mongoc_client_new_from_uri(uri); + mongoc_client_set_error_api(tf->client, MONGOC_ERROR_API_VERSION_2); + ASSERT(mongoc_client_set_oidc_callback(tf->client, oidc_callback)); + } + + mongoc_oidc_callback_destroy(oidc_callback); + mongoc_uri_destroy(uri); + return tf; +} + +static void +test_fixture_destroy(test_fixture_t *tf) +{ + if (!tf) { + return; + } + if (tf->pool) { + mongoc_client_pool_push(tf->pool, tf->client); + mongoc_client_pool_destroy(tf->pool); + } else { + mongoc_client_destroy(tf->client); + } + bson_free(tf); +} + +static bool +do_find(mongoc_client_t *client, bson_error_t *error) +{ + mongoc_collection_t *coll = NULL; + mongoc_cursor_t *cursor = NULL; + bool ret = false; + bson_t filter = BSON_INITIALIZER; + + coll = mongoc_client_get_collection(client, "test", "test"); + cursor = mongoc_collection_find_with_opts(coll, &filter, NULL, NULL); + + const bson_t *doc; + while (mongoc_cursor_next(cursor, &doc)) + ; + + if (mongoc_cursor_error(cursor, error)) { + goto fail; + } + + ret = true; +fail: + mongoc_cursor_destroy(cursor); + mongoc_collection_destroy(coll); + return ret; +} + +static void +configure_failpoint(const char *failpoint_json) +// Configure failpoint on a separate client: +{ + bson_error_t error; + mongoc_client_t *client = test_framework_new_default_client(); + + // Configure fail point: + bson_t *failpoint = tmp_bson(failpoint_json); + + ASSERT_OR_PRINT(mongoc_client_command_simple(client, "admin", failpoint, NULL, NULL, &error), error); + + mongoc_client_destroy(client); +} + +// test_oidc_works tests a simple happy path. +static void +test_oidc_works(void *use_pool_void) +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Expect callback not-yet called: + ASSERT_CMPINT(tf->ctx.call_count, ==, 0); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called: + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +// test_oidc_bad_config tests MONGODB-OIDC with bad configurations. +static void +test_oidc_bad_config(void *unused) +{ + bson_error_t error; + + // Expect error is single-threaded setter used on pooled client: + { + mongoc_uri_t *uri = mongoc_uri_new("mongodb://localhost/?authMechanism=MONGODB-OIDC"); + mongoc_client_pool_t *pool = mongoc_client_pool_new(uri); + mongoc_client_t *client = mongoc_client_pool_pop(pool); + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + capture_logs(true); + ASSERT(!mongoc_client_set_oidc_callback(client, cb)); + ASSERT_CAPTURED_LOG("oidc", MONGOC_LOG_LEVEL_ERROR, "only be used for single threaded clients"); + mongoc_oidc_callback_destroy(cb); + mongoc_client_pool_push(pool, client); + mongoc_client_pool_destroy(pool); + mongoc_uri_destroy(uri); + } + + // Expect error if pool setter used after client is popped: + { + mongoc_uri_t *uri = mongoc_uri_new("mongodb://localhost/?authMechanism=MONGODB-OIDC"); + mongoc_client_pool_t *pool = mongoc_client_pool_new(uri); + mongoc_client_t *client = mongoc_client_pool_pop(pool); + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + capture_logs(true); + ASSERT(!mongoc_client_pool_set_oidc_callback(pool, cb)); + ASSERT_CAPTURED_LOG("oidc", MONGOC_LOG_LEVEL_ERROR, "only be called before mongoc_client_pool_pop"); + mongoc_oidc_callback_destroy(cb); + mongoc_client_pool_push(pool, client); + mongoc_client_pool_destroy(pool); + mongoc_uri_destroy(uri); + } + + // Expect error if no callback set: + { + mongoc_client_t *client = mongoc_client_new("mongodb://localhost/?authMechanism=MONGODB-OIDC"); + bool ok = mongoc_client_command_simple(client, "db", tmp_bson("{'ping': 1}"), NULL, NULL, &error); + ASSERT(!ok); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "no callback set"); + mongoc_client_destroy(client); + } + + // Expect error if callback is set twice: + { + mongoc_client_t *client = mongoc_client_new("mongodb://localhost/?authMechanism=MONGODB-OIDC"); + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + ASSERT(mongoc_client_set_oidc_callback(client, cb)); + capture_logs(true); + ASSERT(!mongoc_client_set_oidc_callback(client, cb)); + ASSERT_CAPTURED_LOG("oidc", MONGOC_LOG_LEVEL_ERROR, "called once"); + mongoc_oidc_callback_destroy(cb); + mongoc_client_destroy(client); + } + + // Expect error if callback is set twice on pool: + { + mongoc_client_pool_t *pool = + mongoc_client_pool_new(mongoc_uri_new("mongodb://localhost/?authMechanism=MONGODB-OIDC")); + mongoc_oidc_callback_t *cb = mongoc_oidc_callback_new(oidc_callback_fn); + ASSERT(mongoc_client_pool_set_oidc_callback(pool, cb)); + capture_logs(true); + ASSERT(!mongoc_client_pool_set_oidc_callback(pool, cb)); + ASSERT_CAPTURED_LOG("oidc", MONGOC_LOG_LEVEL_ERROR, "called once"); + mongoc_oidc_callback_destroy(cb); + mongoc_client_pool_destroy(pool); + } +} + +// test_oidc_delays tests the minimum required time between OIDC calls. +static void +test_oidc_delays(void *use_pool_void) +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Configure failpoint to return ReauthenticationError (391): + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + int64_t start_us = bson_get_monotonic_time(); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + + int64_t end_us = bson_get_monotonic_time(); + + ASSERT_CMPINT64(end_us - start_us, >=, 100 * 1000); // At least 100ms between calls to the callback. + + test_fixture_destroy(tf); +} + +// test_oidc_reauth_twice tests a reauth error occurring twice in a row. +static void +test_oidc_reauth_twice(void *use_pool_void) +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Configure failpoint to return ReauthenticationError (391): + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 2}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + int64_t start_us = bson_get_monotonic_time(); + + // Expect error: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, MONGOC_SERVER_ERR_REAUTHENTICATION_REQUIRED, "failpoint"); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + + int64_t end_us = bson_get_monotonic_time(); + + ASSERT_CMPINT64(end_us - start_us, >=, 100 * 1000); // At least 100ms between calls to the callback. + + test_fixture_destroy(tf); +} + +// test_oidc_reauth_error_v1 tests a reauth error using the V1 error API. +static void +test_oidc_reauth_error_v1(void *use_pool_void) +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool, .use_error_api_v1 = true}); + + // Configure failpoint to return ReauthenticationError (391): + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + int64_t start_us = bson_get_monotonic_time(); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + + int64_t end_us = bson_get_monotonic_time(); + + ASSERT_CMPINT64(end_us - start_us, >=, 100 * 1000); // At least 100ms between calls to the callback. + + test_fixture_destroy(tf); +} + +#define PROSE_TEST(maj, min, desc) static void test_oidc_prose_##maj##_##min(void *use_pool_void) + +PROSE_TEST(1, 1, "Callback is called during authentication") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +static BSON_THREAD_FUN(do_100_finds, pool_void) +{ + mongoc_client_pool_t *pool = pool_void; + for (int i = 0; i < 100; i++) { + mongoc_client_t *client = mongoc_client_pool_pop(pool); + bson_error_t error; + bool ok = do_find(client, &error); + ASSERT_OR_PRINT(ok, error); + mongoc_client_pool_push(pool, client); + } + BSON_THREAD_RETURN; +} + +PROSE_TEST(1, 2, "Callback is called once for multiple connections") +{ + BSON_UNUSED(use_pool_void); // Test only runs for pooled. + bool use_pool = true; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Start 10 threads. Each thread runs 100 find operations: + bson_thread_t threads[10]; + for (int i = 0; i < 10; i++) { + ASSERT(0 == mcommon_thread_create(&threads[i], do_100_finds, tf->pool)); + } + + // Wait for threads to finish: + for (int i = 0; i < 10; i++) { + mcommon_thread_join(threads[i]); + } + + // Expect callback was called. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +PROSE_TEST(2, 1, "Valid Callback Inputs") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = + test_fixture_new((test_config_t){.use_pool = use_pool, .callback_config = {.validate_params = true}}); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + test_fixture_destroy(tf); +} + +PROSE_TEST(2, 2, "OIDC Callback Returns Null") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = + test_fixture_new((test_config_t){.use_pool = use_pool, .callback_config = {.return_null = true}}); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "OIDC callback failed"); + + test_fixture_destroy(tf); +} + +PROSE_TEST(2, 3, "OIDC Callback Returns Missing Data") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){ + .use_pool = use_pool, + .callback_config = { + // mongoc_oidc_credential_t cannot be partially created. Instead of "missing" data, return a bad token. + .return_bad_token = true}}); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 18, "Authentication failed"); + + test_fixture_destroy(tf); +} + +PROSE_TEST(2, 4, "Invalid Client Configuration with Callback") +{ + BSON_UNUSED(use_pool_void); + + mongoc_uri_t *uri = mongoc_uri_new("mongodb://localhost:27017"); + mongoc_uri_set_auth_mechanism(uri, "MONGODB-OIDC"); + mongoc_uri_set_mechanism_properties(uri, tmp_bson(BSON_STR({"ENVIRONMENT" : "test"}))); + + callback_ctx_t ctx; + mongoc_oidc_callback_t *oidc_callback = mongoc_oidc_callback_new(oidc_callback_fn); + mongoc_oidc_callback_set_user_data(oidc_callback, &ctx); + + mongoc_client_t *client = mongoc_client_new_from_uri(uri); + mongoc_client_set_oidc_callback(client, oidc_callback); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Use one or the other"); + + mongoc_client_destroy(client); + mongoc_oidc_callback_destroy(oidc_callback); + mongoc_uri_destroy(uri); +} + +PROSE_TEST(2, 5, "Invalid Client Configuration with Callback") +{ + BSON_UNUSED(use_pool_void); + + bson_error_t error; + mongoc_uri_t *uri = mongoc_uri_new_with_error( + "mongodb://localhost:27017/" + "?retryReads=false&authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,ALLOWED_HOSTS:", + &error); + ASSERT(!uri); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_COMMAND, MONGOC_ERROR_COMMAND_INVALID_ARG, "Unsupported"); + mongoc_uri_destroy(uri); +} + +static void +poison_client_cache(mongoc_client_t *client) +{ + BSON_ASSERT_PARAM(client); + mongoc_oidc_cache_set_cached_token(client->topology->oidc_cache, "bad_token"); +} + +PROSE_TEST(3, 1, "Authentication failure with cached tokens fetch a new token and retry auth") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + poison_client_cache(tf->client); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +PROSE_TEST(3, 2, "Authentication failures without cached tokens return an error") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = + test_fixture_new((test_config_t){.use_pool = use_pool, .callback_config = {.return_bad_token = true}}); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 18, "Authentication failed"); + + // Expect callback was called. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +PROSE_TEST(3, 3, "Unexpected error code does not clear the cache") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["saslStart"], "errorCode" : 20} + })); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 20, "Failing command"); + + // Expect callback was called. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + // Expect second attempt succeeds: + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was not called again. + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + test_fixture_destroy(tf); +} + +PROSE_TEST(4, 1, "Reauthentication Succeeds") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + // Expect auth to succeed: + bson_error_t error; + ASSERT_OR_PRINT(do_find(tf->client, &error), error); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + + test_fixture_destroy(tf); +} + +PROSE_TEST(4, 2, "Read Commands Fail If Reauthentication Fails") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new( + (test_config_t){.use_pool = use_pool, .callback_config = {.return_bad_token_after_first_call = true}}); + + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_find(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 18, "Authentication failed"); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + test_fixture_destroy(tf); +} + +static bool +do_insert(mongoc_client_t *client, bson_error_t *error) +{ + mongoc_collection_t *coll = NULL; + bool ret = false; + bson_t doc = BSON_INITIALIZER; + + coll = mongoc_client_get_collection(client, "test", "test"); + if (!mongoc_collection_insert_one(coll, &doc, NULL, NULL, error)) { + goto fail; + } + + ret = true; +fail: + mongoc_collection_destroy(coll); + return ret; +} + +PROSE_TEST(4, 3, "Write Commands Fail If Reauthentication Fails") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new( + (test_config_t){.use_pool = use_pool, .callback_config = {.return_bad_token_after_first_call = true}}); + + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["insert"], "errorCode" : 391} + })); + + // Expect auth to fail: + bson_error_t error; + ASSERT(!do_insert(tf->client, &error)); + ASSERT_ERROR_CONTAINS(error, MONGOC_ERROR_SERVER, 18, "Authentication failed"); + + // Expect callback was called twice: once for initial auth, once for reauth. + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + test_fixture_destroy(tf); +} + +// If counters are enabled, define operation count checks: +#ifdef MONGOC_ENABLE_SHM_COUNTERS +#define DECL_OPCOUNT() int32_t opcount = mongoc_counter_op_egress_total_count() +#define ASSERT_OPCOUNT(x) ASSERT_CMPINT32(mongoc_counter_op_egress_total_count(), ==, opcount + x) +#else +#define DECL_OPCOUNT() ((void)0) +#define ASSERT_OPCOUNT(x) ((void)0) +#endif + +static void +populate_client_cache(mongoc_client_t *client) +{ + BSON_ASSERT_PARAM(client); + char *access_token = read_test_token(); + mongoc_oidc_cache_set_cached_token(client->topology->oidc_cache, access_token); + bson_free(access_token); +} + +PROSE_TEST(4, 4, "Speculative Authentication should be ignored on Reauthentication") +{ + BSON_UNUSED(use_pool_void); + bool use_pool = false; // Only run on single to avoid counters being updated by background threads. + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + bson_error_t error; + + // Populate client cache with a valid access token to enforce speculative authentication: + populate_client_cache(tf->client); + + // Expect successful auth without sending saslStart: + { + DECL_OPCOUNT(); + + // Expect auth to succeed: + ASSERT_OR_PRINT(do_insert(tf->client, &error), error); + + // Expect callback was not called: + ASSERT_CMPINT(tf->ctx.call_count, ==, 0); + + // Expect two commands sent: hello + insert. + // Expect saslStart was not sent. + // TODO(CDRIVER-2669): check command started events instead of counters. + ASSERT_OPCOUNT(2); + } + + // Expect successful reauth with sending saslStart: + { + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["insert"], "errorCode" : 391} + })); + + DECL_OPCOUNT(); + + // Expect auth to succeed (after reauth): + ASSERT_OR_PRINT(do_insert(tf->client, &error), error); + + // Expect callback was called: + ASSERT_CMPINT(tf->ctx.call_count, ==, 1); + + // Check that three commands were sent: insert (fails) + saslStart + insert (succeeds). + // TODO(CDRIVER-2669): check command started events instead. + ASSERT_OPCOUNT(3); + } + + test_fixture_destroy(tf); +} + +static bool +do_find_with_session(mongoc_client_t *client, bson_error_t *error) +{ + mongoc_collection_t *coll = NULL; + mongoc_cursor_t *cursor = NULL; + bool ret = false; + bson_t filter = BSON_INITIALIZER; + bson_t opts = BSON_INITIALIZER; + mongoc_client_session_t *sess = NULL; + + // Create session: + sess = mongoc_client_start_session(client, NULL, error); + if (!sess) { + goto fail; + } + + if (!mongoc_client_session_append(sess, &opts, error)) { + goto fail; + } + + coll = mongoc_client_get_collection(client, "test", "test"); + cursor = mongoc_collection_find_with_opts(coll, &filter, &opts, NULL); + + const bson_t *doc; + while (mongoc_cursor_next(cursor, &doc)) + ; + + if (mongoc_cursor_error(cursor, error)) { + goto fail; + } + + ret = true; +fail: + mongoc_client_session_destroy(sess); + bson_destroy(&opts); + mongoc_cursor_destroy(cursor); + mongoc_collection_destroy(coll); + return ret; +} + +PROSE_TEST(4, 5, "Reauthentication Succeeds when a Session is involved") +{ + bool use_pool = *(bool *)use_pool_void; + test_fixture_t *tf = test_fixture_new((test_config_t){.use_pool = use_pool}); + + // Configure failpoint: + configure_failpoint(BSON_STR({ + "configureFailPoint" : "failCommand", + "mode" : {"times" : 1}, + "data" : {"failCommands" : ["find"], "errorCode" : 391} + })); + + // Expect find on a session succeeds: + bson_error_t error; + ASSERT_OR_PRINT(do_find_with_session(tf->client, &error), error); + + // Expect callback was called twice: + ASSERT_CMPINT(tf->ctx.call_count, ==, 2); + + test_fixture_destroy(tf); +} + +static int +skip_if_no_oidc(void) +{ + return test_framework_is_oidc() ? 1 : 0; +} + +void +test_oidc_auth_install(TestSuite *suite) +{ + static bool single = false; + static bool pooled = true; + + TestSuite_AddFull(suite, "/oidc/bad_config", test_oidc_bad_config, NULL, NULL, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/works/single", test_oidc_works, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/works/pooled", test_oidc_works, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/delays/single", test_oidc_delays, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/delays/pooled", test_oidc_delays, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/reauth_twice/single", test_oidc_reauth_twice, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/reauth_twice/pooled", test_oidc_reauth_twice, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/reauth_error_v1/single", test_oidc_reauth_error_v1, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/reauth_error_v1/pooled", test_oidc_reauth_error_v1, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/1.1/single", test_oidc_prose_1_1, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/1.1/pooled", test_oidc_prose_1_1, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/1.2", test_oidc_prose_1_2, NULL, NULL, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/2.1/single", test_oidc_prose_2_1, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/2.1/pooled", test_oidc_prose_2_1, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/2.2/single", test_oidc_prose_2_2, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/2.2/pooled", test_oidc_prose_2_2, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/2.3/single", test_oidc_prose_2_3, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/2.3/pooled", test_oidc_prose_2_3, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/2.4", test_oidc_prose_2_4, NULL, NULL, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/2.5", test_oidc_prose_2_5, NULL, NULL, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/3.1/single", test_oidc_prose_3_1, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/3.1/pooled", test_oidc_prose_3_1, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/3.2/single", test_oidc_prose_3_2, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/3.2/pooled", test_oidc_prose_3_2, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/3.3/single", test_oidc_prose_3_3, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/3.3/pooled", test_oidc_prose_3_3, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/4.1/single", test_oidc_prose_4_1, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/4.1/pooled", test_oidc_prose_4_1, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/4.2/single", test_oidc_prose_4_2, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/4.2/pooled", test_oidc_prose_4_2, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/4.3/single", test_oidc_prose_4_3, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/4.3/pooled", test_oidc_prose_4_3, NULL, &pooled, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/4.4", test_oidc_prose_4_4, NULL, NULL, skip_if_no_oidc); + + TestSuite_AddFull(suite, "/oidc/prose/4.5/single", test_oidc_prose_4_5, NULL, &single, skip_if_no_oidc); + TestSuite_AddFull(suite, "/oidc/prose/4.5/pooled", test_oidc_prose_4_5, NULL, &pooled, skip_if_no_oidc); +} diff --git a/src/libmongoc/tests/unified/entity-map.c b/src/libmongoc/tests/unified/entity-map.c index aea885a4cd0..ca6f21c387f 100644 --- a/src/libmongoc/tests/unified/entity-map.c +++ b/src/libmongoc/tests/unified/entity-map.c @@ -123,7 +123,28 @@ uri_apply_options(mongoc_uri_t *uri, bson_t *opts, bson_error_t *error) mongoc_uri_set_appname(uri, bson_iter_utf8(&iter, NULL)); } else if (0 == bson_strcasecmp(MONGOC_URI_SERVERMONITORINGMODE, key)) { mongoc_uri_set_option_as_utf8(uri, key, bson_iter_utf8(&iter, NULL)); - } else { + } else if (0 == bson_strcasecmp(MONGOC_URI_AUTHMECHANISM, key)) { + mongoc_uri_set_auth_mechanism(uri, bson_iter_utf8(&iter, NULL)); + } else if (0 == bson_strcasecmp(MONGOC_URI_AUTHMECHANISMPROPERTIES, key)) { + bson_t props; + if (!_mongoc_iter_document_as_bson(&iter, &props, error)) { + goto done; + } + bson_t *expect = BCON_NEW("$$placeholder", BCON_INT32(1)); + if (!bson_equal(&props, expect)) { + test_set_error(error, "expected authMechanismProperties to be placeholder"); + bson_destroy(expect); + goto done; + } + + if (!test_framework_is_oidc()) { + test_set_error(error, "expected test with authMechanismProperties to only apply to OIDC"); + bson_destroy(expect); + goto done; + } + } + + else { test_set_error(error, "Unimplemented test runner support for URI option: %s", key); goto done; } @@ -875,8 +896,14 @@ entity_client_new(entity_map_t *em, bson_t *bson, bson_error_t *error) test_error("Error while parsing entity object: %s", bsonParseError); } - /* Build the client's URI. */ - uri = test_framework_get_uri(); + // Build client's URI: + { + char *uri_noauth = test_framework_get_uri_str_no_auth(NULL); // Apply auth later. + uri = mongoc_uri_new(uri_noauth); + ASSERT(uri); + bson_free(uri_noauth); + } + /* Apply "useMultipleMongoses" rules to URI. * If useMultipleMongoses is true, modify the connection string to add a * host. If useMultipleMongoses is false, require that the connection string @@ -906,6 +933,21 @@ entity_client_new(entity_map_t *em, bson_t *bson, bson_error_t *error) } } + // Apply username/password (if applicable), unless URI requests OIDC. + const char *auth_mech = mongoc_uri_get_auth_mechanism(uri); + bool uri_requests_oidc = auth_mech && 0 == strcmp(auth_mech, "MONGODB-OIDC"); + if (!uri_requests_oidc) { + char *test_username = test_framework_get_admin_user(); + char *test_password = test_framework_get_admin_password(); + if (test_username && test_password) { + // Test environment indicates server supports auth. + mongoc_uri_set_username(uri, test_username); + mongoc_uri_set_password(uri, test_password); + } + bson_free(test_username); + bson_free(test_password); + } + if (!mongoc_uri_has_option(uri, MONGOC_URI_HEARTBEATFREQUENCYMS)) { can_reduce_heartbeat = true; } @@ -915,6 +957,11 @@ entity_client_new(entity_map_t *em, bson_t *bson, bson_error_t *error) } client = test_framework_client_new_from_uri(uri, api); + + if (uri_requests_oidc) { + test_framework_set_oidc_callback(client); + } + test_framework_set_ssl_opts(client); mongoc_client_set_error_api(client, MONGOC_ERROR_API_VERSION_2); entity->value = client; diff --git a/src/libmongoc/tests/unified/runner.c b/src/libmongoc/tests/unified/runner.c index 0d15ad74673..731f00b3718 100644 --- a/src/libmongoc/tests/unified/runner.c +++ b/src/libmongoc/tests/unified/runner.c @@ -327,6 +327,12 @@ test_runner_terminate_open_transactions(test_runner_t *test_runner, bson_error_t bool cmd_ret = false; bson_error_t cmd_error = {0}; + if (test_framework_is_oidc()) { + // Skip when testing OIDC to avoid authorization error trying to run killAllSessions. + ret = true; + goto done; + } + if (0 == test_framework_skip_if_no_txns()) { ret = true; goto done; @@ -423,7 +429,8 @@ test_runner_new(void) test_error("error terminating transactions: %s", error.message); } - { + // Do not check server parameters when testing OIDC to avoid authorization error running getParameter. + if (!test_framework_is_oidc()) { bson_t reply; /* Cache server parameters to check runOnRequirements. */ if (!mongoc_client_command_simple( @@ -749,6 +756,24 @@ check_run_on_requirement(test_runner_t *test_runner, return false; } + if (0 == strcmp(key, "authMechanism")) { + if (!BSON_ITER_HOLDS_UTF8(&req_iter)) { + test_error("Unexpected type for authMechanism, should be string"); + } + const char *mechanism = bson_iter_utf8(&req_iter, NULL); + + if (strcasecmp(mechanism, "MONGODB-OIDC") != 0) { + test_error("Unexpected authMechanism value: %s", mechanism); + } + + if (test_framework_is_oidc()) { + continue; + } + + *fail_reason = bson_strdup("Test requires environment to support MONGODB-OIDC"); + return false; + } + #if defined(MONGOC_ENABLE_CLIENT_SIDE_ENCRYPTION) if (0 == strcmp(key, "csfle")) { bool csfle_required = false; @@ -2227,4 +2252,6 @@ test_install_unified(TestSuite *suite) run_unified_tests(suite, JSON_DIR, "server_selection/logging"); run_unified_tests(suite, JSON_DIR, "server_discovery_and_monitoring/unified"); + + run_unified_tests(suite, JSON_DIR, "auth/unified"); } From 6de89e85f3d688e9c875d9d433325eed418a22f7 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Thu, 16 Oct 2025 14:51:29 -0400 Subject: [PATCH 04/11] remove extra newline Co-authored-by: mdb-ad <198671546+mdb-ad@users.noreply.github.com> --- src/libmongoc/src/mongoc/mongoc-cluster-private.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libmongoc/src/mongoc/mongoc-cluster-private.h b/src/libmongoc/src/mongoc/mongoc-cluster-private.h index 09c1464202b..41b25ea147f 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster-private.h +++ b/src/libmongoc/src/mongoc/mongoc-cluster-private.h @@ -82,8 +82,6 @@ mongoc_cluster_reset_sockettimeoutms(mongoc_cluster_t *cluster); void mongoc_cluster_disconnect_node(mongoc_cluster_t *cluster, uint32_t id); - - int32_t mongoc_cluster_get_max_bson_obj_size(mongoc_cluster_t *cluster); From 0286bcf53ccd3d4181b5a60bfad3f3de24eddb0a Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Thu, 16 Oct 2025 14:55:50 -0400 Subject: [PATCH 05/11] remove unused headers --- src/libmongoc/src/mongoc/mongoc-topology-private.h | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/libmongoc/src/mongoc/mongoc-topology-private.h b/src/libmongoc/src/mongoc/mongoc-topology-private.h index 2a8051ff6c7..ff340f7d515 100644 --- a/src/libmongoc/src/mongoc/mongoc-topology-private.h +++ b/src/libmongoc/src/mongoc/mongoc-topology-private.h @@ -21,7 +21,6 @@ #include #include -#include #include #include #include @@ -34,7 +33,6 @@ #include #include -#include #include #include From d486c92120bce2aba87b01dba736db0e9dff3ce4 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 08:46:58 -0400 Subject: [PATCH 06/11] fix `evergreen validate` warning > WARNING: task group 'test-oidc-task-group' has a teardown task timeout of 3600 seconds, which exceeds the maximum of 180 seconds --- .evergreen/config_generator/components/oidc.py | 2 +- .evergreen/generated_configs/task_groups.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.evergreen/config_generator/components/oidc.py b/.evergreen/config_generator/components/oidc.py index 73f51c296f2..5cc7a747042 100644 --- a/.evergreen/config_generator/components/oidc.py +++ b/.evergreen/config_generator/components/oidc.py @@ -19,7 +19,7 @@ def task_groups(): setup_group_can_fail_task=True, setup_group_timeout_secs=60 * 60, # 1 hour teardown_group_can_fail_task=True, - teardown_group_timeout_secs=60 * 60, # 1 hour + teardown_group_timeout_secs=180, # 3 minutes setup_group=[ FetchDET.call(), ec2_assume_role(role_arn='${aws_test_secrets_role}'), diff --git a/.evergreen/generated_configs/task_groups.yml b/.evergreen/generated_configs/task_groups.yml index 46518092f86..eb5085e801c 100644 --- a/.evergreen/generated_configs/task_groups.yml +++ b/.evergreen/generated_configs/task_groups.yml @@ -28,4 +28,4 @@ task_groups: args: - -c - ./drivers-evergreen-tools/.evergreen/auth_oidc/teardown.sh - teardown_group_timeout_secs: 3600 + teardown_group_timeout_secs: 180 From f909c8f32ebe2b032fd5af969334090b0df979d7 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 13:10:56 -0400 Subject: [PATCH 07/11] destroy `expect` document in tests Co-authored-by: Connor MacDonald --- src/libmongoc/tests/unified/entity-map.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libmongoc/tests/unified/entity-map.c b/src/libmongoc/tests/unified/entity-map.c index ca6f21c387f..6416312db81 100644 --- a/src/libmongoc/tests/unified/entity-map.c +++ b/src/libmongoc/tests/unified/entity-map.c @@ -142,6 +142,8 @@ uri_apply_options(mongoc_uri_t *uri, bson_t *opts, bson_error_t *error) bson_destroy(expect); goto done; } + + bson_destroy(expect); } else { From 1c7ff5fe8f879ef0e0fea3d3ea322b96f7b8030e Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 13:03:03 -0400 Subject: [PATCH 08/11] remove unhelpful comment --- src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h b/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h index 5839d85387f..2d563278b66 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h +++ b/src/libmongoc/src/mongoc/mongoc-cluster-oidc-private.h @@ -26,7 +26,6 @@ struct _mongoc_cluster_t; // Forward declare. #include -// mongoc_oidc_append_speculative_auth adds speculative auth. bool mongoc_oidc_append_speculative_auth(const char *access_token, uint32_t server_id, bson_t *cmd, bson_error_t *error); From 02bac5bb44e837412800a2d29d9d3209ac4701a6 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 13:04:53 -0400 Subject: [PATCH 09/11] flatten `mongoc_cluster_run_command_monitored` --- src/libmongoc/src/mongoc/mongoc-cluster.c | 49 ++++++++++++----------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/libmongoc/src/mongoc/mongoc-cluster.c b/src/libmongoc/src/mongoc/mongoc-cluster.c index f433a3377e1..ef451ac70ab 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster.c +++ b/src/libmongoc/src/mongoc/mongoc-cluster.c @@ -697,34 +697,35 @@ _try_get_oidc_connection_cache(mongoc_cluster_t *cluster, uint32_t server_id, bs bool mongoc_cluster_run_command_monitored(mongoc_cluster_t *cluster, mongoc_cmd_t *cmd, bson_t *reply, bson_error_t *error) { - bool ok = run_command_monitored(cluster, cmd, reply, error); - if (!ok) { - const char *mechanism = mongoc_uri_get_auth_mechanism(cluster->uri); - bool using_oidc = mechanism && 0 == strcasecmp(mechanism, "MONGODB-OIDC"); - - // From auth spec: - // > If any operation fails with `ReauthenticationRequired` (error code 391) and MONGODB-OIDC is in use, the - // > driver MUST reauthenticate the connection. - if (using_oidc && _mongoc_error_is_reauth(error, cluster->client->error_api_version)) { - if (reply) { - bson_destroy(reply); - bson_init(reply); - } + if (run_command_monitored(cluster, cmd, reply, error)) { + return true; + } - mongoc_oidc_connection_cache_t *oidc_connection_cache = - _try_get_oidc_connection_cache(cluster, cmd->server_stream->sd->id, error); - if (!oidc_connection_cache) { - return false; - } + const char *mechanism = mongoc_uri_get_auth_mechanism(cluster->uri); + bool using_oidc = mechanism && 0 == strcasecmp(mechanism, "MONGODB-OIDC"); - if (!_mongoc_cluster_reauth_node_oidc( - cluster, cmd->server_stream->stream, oidc_connection_cache, cmd->server_stream->sd, error)) { - return false; - } - return run_command_monitored(cluster, cmd, reply, error); + // From auth spec: + // > If any operation fails with `ReauthenticationRequired` (error code 391) and MONGODB-OIDC is in use, the + // > driver MUST reauthenticate the connection. + if (using_oidc && _mongoc_error_is_reauth(error, cluster->client->error_api_version)) { + if (reply) { + bson_destroy(reply); + bson_init(reply); + } + + mongoc_oidc_connection_cache_t *oidc_connection_cache = + _try_get_oidc_connection_cache(cluster, cmd->server_stream->sd->id, error); + if (!oidc_connection_cache) { + return false; } + + if (!_mongoc_cluster_reauth_node_oidc( + cluster, cmd->server_stream->stream, oidc_connection_cache, cmd->server_stream->sd, error)) { + return false; + } + return run_command_monitored(cluster, cmd, reply, error); } - return ok; + return false; } From 0070f5fedc92575878183f126235afb004b476b0 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 13:08:49 -0400 Subject: [PATCH 10/11] remove superflous braces --- src/libmongoc/src/mongoc/mongoc-cluster.c | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/libmongoc/src/mongoc/mongoc-cluster.c b/src/libmongoc/src/mongoc/mongoc-cluster.c index ef451ac70ab..0519ec0a179 100644 --- a/src/libmongoc/src/mongoc/mongoc-cluster.c +++ b/src/libmongoc/src/mongoc/mongoc-cluster.c @@ -1844,17 +1844,15 @@ _mongoc_cluster_finish_speculative_auth(mongoc_cluster_t *cluster, if (strcasecmp(mechanism, "MONGODB-OIDC") == 0) { // Expect successful reply to include `done: true`: - { - auth_handled = true; - - bsonParse(*speculative_auth_response, require(allOf(key("done"), isTrue), nop)); - if (bsonParseError) { - _mongoc_set_error( - error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Error in OIDC reply: %s", bsonParseError); - ret = false; - } else { - ret = true; - } + auth_handled = true; + + bsonParse(*speculative_auth_response, require(allOf(key("done"), isTrue), nop)); + if (bsonParseError) { + _mongoc_set_error( + error, MONGOC_ERROR_CLIENT, MONGOC_ERROR_CLIENT_AUTHENTICATE, "Error in OIDC reply: %s", bsonParseError); + ret = false; + } else { + ret = true; } } From 4418cd670e5a757c95faa94e72c5ea896c09b924 Mon Sep 17 00:00:00 2001 From: Kevin Albertson Date: Fri, 17 Oct 2025 14:53:10 -0400 Subject: [PATCH 11/11] format --- src/libmongoc/tests/unified/entity-map.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libmongoc/tests/unified/entity-map.c b/src/libmongoc/tests/unified/entity-map.c index 6416312db81..089d033a768 100644 --- a/src/libmongoc/tests/unified/entity-map.c +++ b/src/libmongoc/tests/unified/entity-map.c @@ -142,8 +142,8 @@ uri_apply_options(mongoc_uri_t *uri, bson_t *opts, bson_error_t *error) bson_destroy(expect); goto done; } - - bson_destroy(expect); + + bson_destroy(expect); } else {