From 51718d867446fef2a443512b8399827fbcd00aca Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 Nov 2023 13:43:22 -0500 Subject: [PATCH] feat(NODE-5464): OIDC machine workflow --- .evergreen/config.in.yml | 149 +++- .evergreen/config.yml | 197 ++++- .evergreen/generate_evergreen_tasks.js | 70 +- .evergreen/prepare-shell.sh | 2 +- .evergreen/run-oidc-auth-tests.sh | 38 + .evergreen/run-oidc-tests-azure.sh | 3 +- .evergreen/run-oidc-tests-gcp.sh | 10 + .evergreen/run-oidc-tests.sh | 35 +- package.json | 2 + src/cmap/auth/mongo_credentials.ts | 40 +- src/cmap/auth/mongodb_oidc.ts | 59 +- ...ce_workflow.ts => aws_machine_workflow.ts} | 4 +- ..._workflow.ts => azure_machine_workflow.ts} | 42 +- .../auth/mongodb_oidc/azure_token_cache.ts | 51 -- src/cmap/auth/mongodb_oidc/cache.ts | 63 -- .../auth/mongodb_oidc/callback_lock_cache.ts | 114 --- .../auth/mongodb_oidc/callback_workflow.ts | 260 ++---- .../auth/mongodb_oidc/command_builders.ts | 43 + .../auth/mongodb_oidc/gcp_machine_workflow.ts | 48 ++ .../auth/mongodb_oidc/machine_workflow.ts | 73 ++ .../auth/mongodb_oidc/service_workflow.ts | 49 -- src/cmap/auth/mongodb_oidc/token_cache.ts | 26 + .../auth/mongodb_oidc/token_entry_cache.ts | 77 -- src/error.ts | 28 + src/index.ts | 10 +- src/mongo_client.ts | 3 + src/mongo_client_auth_providers.ts | 3 +- .../auth/mongodb_oidc_azure.prose.test.ts | 23 +- .../auth/mongodb_oidc_gcp.prose.test.ts | 35 + test/manual/mongodb_oidc.prose.test.ts | 812 +++++------------- test/mongodb.ts | 10 +- test/spec/auth/legacy/connection-string.json | 91 +- test/spec/auth/legacy/connection-string.yml | 84 +- ...h_retry.json => oidc-auth-with-retry.json} | 65 +- ...ith_retry.yml => oidc-auth-with-retry.yml} | 38 +- ...etry.json => oidc-auth-without-retry.json} | 76 +- ..._retry.yml => oidc-auth-without-retry.yml} | 46 +- test/tools/runner/config.ts | 9 + test/tools/runner/hooks/configuration.js | 7 - test/tools/unified-spec-runner/runner.ts | 7 + test/tools/unified-spec-runner/schema.ts | 1 + .../unified-spec-runner/unified-utils.ts | 18 +- test/tools/uri_spec_runner.ts | 11 +- ...w.test.ts => aws_machine_workflow.test.ts} | 6 +- .../azure_machine_workflow.test.ts | 24 + .../mongodb_oidc/azure_token_cache.test.ts | 77 -- .../mongodb_oidc/callback_lock_cache.test.ts | 145 ---- .../mongodb_oidc/gcp_machine_workflow.test.ts | 24 + .../mongodb_oidc/token_entry_cache.test.ts | 144 ---- 49 files changed, 1290 insertions(+), 1962 deletions(-) create mode 100755 .evergreen/run-oidc-auth-tests.sh create mode 100644 .evergreen/run-oidc-tests-gcp.sh rename src/cmap/auth/mongodb_oidc/{aws_service_workflow.ts => aws_machine_workflow.ts} (84%) rename src/cmap/auth/mongodb_oidc/{azure_service_workflow.ts => azure_machine_workflow.ts} (64%) delete mode 100644 src/cmap/auth/mongodb_oidc/azure_token_cache.ts delete mode 100644 src/cmap/auth/mongodb_oidc/cache.ts delete mode 100644 src/cmap/auth/mongodb_oidc/callback_lock_cache.ts create mode 100644 src/cmap/auth/mongodb_oidc/command_builders.ts create mode 100644 src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts create mode 100644 src/cmap/auth/mongodb_oidc/machine_workflow.ts delete mode 100644 src/cmap/auth/mongodb_oidc/service_workflow.ts create mode 100644 src/cmap/auth/mongodb_oidc/token_cache.ts delete mode 100644 src/cmap/auth/mongodb_oidc/token_entry_cache.ts create mode 100644 test/integration/auth/mongodb_oidc_gcp.prose.test.ts rename test/spec/auth/unified/{reauthenticate_with_retry.json => oidc-auth-with-retry.json} (72%) rename test/spec/auth/unified/{reauthenticate_with_retry.yml => oidc-auth-with-retry.yml} (71%) rename test/spec/auth/unified/{reauthenticate_without_retry.json => oidc-auth-without-retry.json} (69%) rename test/spec/auth/unified/{reauthenticate_without_retry.yml => oidc-auth-without-retry.yml} (68%) rename test/unit/cmap/auth/mongodb_oidc/{aws_service_workflow.test.ts => aws_machine_workflow.test.ts} (85%) create mode 100644 test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts create mode 100644 test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 6ac4ff68f5d..53c0932c7ff 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -171,10 +171,29 @@ functions: ${PREPARE_SHELL} OIDC_TOKEN_DIR="/tmp/tokens" \ + PROVIDER_NAME="aws" \ AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ + OIDC_ATLAS_URI_SINGLE="${OIDC_ATLAS_URI_SINGLE}" \ + OIDC_ATLAS_URI_MULTI="${OIDC_ATLAS_URI_MULTI}" \ PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh + "run oidc auth tests aws": + - command: shell.exec + type: test + params: + working_dir: "src" + timeout_secs: 300 + shell: bash + script: | + ${PREPARE_SHELL} + + OIDC_TOKEN_DIR="/tmp/tokens" \ + PROVIDER_NAME="aws" \ + AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ + PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ + bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-auth-tests.sh + "run tests": - command: shell.exec type: test @@ -1258,11 +1277,21 @@ tasks: - name: "oidc-auth-test-azure-latest" commands: - - command: expansions.update - type: setup + - func: "install dependencies" + - command: subprocess.exec params: - updates: - - { key: NPM_VERSION, value: "9" } + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: azure + SCRIPT: run-oidc-tests.sh + args: + - .evergreen/run-oidc-tests-azure.sh + + - name: "oidc-auth-test-azure-latest-auth" + commands: - func: "install dependencies" - command: subprocess.exec params: @@ -1271,11 +1300,41 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} PROVIDER_NAME: azure + SCRIPT: run-oidc-auth-tests.sh args: - .evergreen/run-oidc-tests-azure.sh + - name: "oidc-auth-test-gcp-latest" + commands: + - func: "install dependencies" + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: gcp + SCRIPT: run-oidc-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + + - name: "oidc-auth-test-gcp-latest-auth" + commands: + - func: "install dependencies" + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: gcp + SCRIPT: run-oidc-auth-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + - name: "test-aws-lambda-deployed" commands: - command: expansions.update @@ -1427,14 +1486,9 @@ task_groups: script: |- set -o errexit ${PREPARE_SHELL} - export AZUREOIDC_CLIENTID="${testazureoidc_clientid}" - export AZUREOIDC_TENANTID="${testazureoic_tenantid}" - export AZUREOIDC_SECRET="${testazureoidc_secret}" - export AZUREOIDC_KEYVAULT=${testazureoidc_keyvault} - export AZUREOIDC_DRIVERS_TOOLS="$DRIVERS_TOOLS" export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER" $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh - teardown_group: + teardown_task: - command: shell.exec params: shell: bash @@ -1446,6 +1500,75 @@ task_groups: tasks: - oidc-auth-test-azure-latest + - name: testazureoidcauth_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-azure-latest-auth + + - name: testgcpoidc_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/create-and-setup-instance.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/delete-instance.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + + - name: testgcpoidcauth_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/create-and-setup-instance.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/delete-instance.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest-auth + - name: test_atlas_task_group setup_group: - func: fetch source @@ -1461,7 +1584,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -1489,7 +1612,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src diff --git a/.evergreen/config.yml b/.evergreen/config.yml index c1056a6f0b4..a18ee783a75 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -142,9 +142,27 @@ functions: ${PREPARE_SHELL} OIDC_TOKEN_DIR="/tmp/tokens" \ + PROVIDER_NAME="aws" \ AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ + OIDC_ATLAS_URI_SINGLE="${OIDC_ATLAS_URI_SINGLE}" \ + OIDC_ATLAS_URI_MULTI="${OIDC_ATLAS_URI_MULTI}" \ PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh + run oidc auth tests aws: + - command: shell.exec + type: test + params: + working_dir: src + timeout_secs: 300 + shell: bash + script: | + ${PREPARE_SHELL} + + OIDC_TOKEN_DIR="/tmp/tokens" \ + PROVIDER_NAME="aws" \ + AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ + PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ + bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-auth-tests.sh run tests: - command: shell.exec type: test @@ -1209,11 +1227,20 @@ tasks: - src/.evergreen/run-azure-kms-tests.sh - name: oidc-auth-test-azure-latest commands: - - command: expansions.update - type: setup + - func: install dependencies + - command: subprocess.exec params: - updates: - - {key: NPM_VERSION, value: '9'} + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: azure + SCRIPT: run-oidc-tests.sh + args: + - .evergreen/run-oidc-tests-azure.sh + - name: oidc-auth-test-azure-latest-auth + commands: - func: install dependencies - command: subprocess.exec params: @@ -1222,10 +1249,38 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} PROVIDER_NAME: azure + SCRIPT: run-oidc-auth-tests.sh args: - .evergreen/run-oidc-tests-azure.sh + - name: oidc-auth-test-gcp-latest + commands: + - func: install dependencies + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: gcp + SCRIPT: run-oidc-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + - name: oidc-auth-test-gcp-latest-auth + commands: + - func: install dependencies + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + PROVIDER_NAME: gcp + SCRIPT: run-oidc-auth-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh - name: test-aws-lambda-deployed commands: - command: expansions.update @@ -1837,6 +1892,25 @@ tasks: - func: bootstrap mongo-orchestration - func: setup oidc roles - func: run oidc tests aws + - name: test-auth-oidc-aws + tags: + - latest + - replica_set + - oidc + commands: + - command: expansions.update + type: setup + params: + updates: + - {key: VERSION, value: latest} + - {key: TOPOLOGY, value: replica_set} + - {key: AUTH, value: auth} + - {key: ORCHESTRATION_FILE, value: auth-oidc.json} + - func: install dependencies + - func: bootstrap oidc + - func: bootstrap mongo-orchestration + - func: setup oidc roles + - func: run oidc auth tests aws - name: test-socks5 tags: [] commands: @@ -3814,14 +3888,9 @@ task_groups: script: |- set -o errexit ${PREPARE_SHELL} - export AZUREOIDC_CLIENTID="${testazureoidc_clientid}" - export AZUREOIDC_TENANTID="${testazureoic_tenantid}" - export AZUREOIDC_SECRET="${testazureoidc_secret}" - export AZUREOIDC_KEYVAULT=${testazureoidc_keyvault} - export AZUREOIDC_DRIVERS_TOOLS="$DRIVERS_TOOLS" export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER" $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh - teardown_group: + teardown_task: - command: shell.exec params: shell: bash @@ -3832,6 +3901,72 @@ task_groups: setup_group_timeout_secs: 1800 tasks: - oidc-auth-test-azure-latest + - name: testazureoidcauth_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export AZUREOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/create-and-setup-vm.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-azure-latest-auth + - name: testgcpoidc_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/create-and-setup-instance.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/delete-instance.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + - name: testgcpoidcauth_task_group + setup_group: + - func: fetch source + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + export GCPOIDC_VMNAME_PREFIX="NODE_DRIVER" + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/create-and-setup-instance.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/delete-instance.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest-auth - name: test_atlas_task_group setup_group: - func: fetch source @@ -3847,7 +3982,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -3874,7 +4009,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -3947,6 +4082,7 @@ buildvariants: - test-auth-kerberos - test-auth-ldap - test-auth-oidc + - test-auth-oidc-aws - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -3998,6 +4134,7 @@ buildvariants: - test-auth-kerberos - test-auth-ldap - test-auth-oidc + - test-auth-oidc-aws - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4049,6 +4186,7 @@ buildvariants: - test-auth-kerberos - test-auth-ldap - test-auth-oidc + - test-auth-oidc-aws - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4099,6 +4237,7 @@ buildvariants: - test-auth-kerberos - test-auth-ldap - test-auth-oidc + - test-auth-oidc-aws - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4386,6 +4525,38 @@ buildvariants: tasks: - test_azurekms_task_group - test-azurekms-fail-task + - name: ubuntu20-test-azure-oidc + display_name: Azure OIDC + run_on: ubuntu2004-small + expansions: + NODE_LTS_VERSION: 20 + batchtime: 20160 + tasks: + - testazureoidc_task_group + - name: ubuntu20-test-azure-oidc-auth + display_name: Azure OIDC Unified Tests + run_on: ubuntu2004-small + expansions: + NODE_LTS_VERSION: 20 + batchtime: 20160 + tasks: + - testazureoidcauth_task_group + - name: ubuntu20-test-gcp-oidc + display_name: GCP OIDC + expansions: + NODE_LTS_VERSION: 20 + run_on: ubuntu2004-small + batchtime: 20160 + tasks: + - testgcpoidc_task_group + - name: ubuntu20-test-gcp-oidc-auth + display_name: GCP OIDC Unified Tests + expansions: + NODE_LTS_VERSION: 20 + run_on: ubuntu2004-small + batchtime: 20160 + tasks: + - testgcpoidcauth_task_group - name: rhel8-test-atlas display_name: Atlas Cluster Tests run_on: rhel80-large diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 89d2eb7c889..df12ff6f506 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -199,6 +199,23 @@ TASKS.push( { func: 'run oidc tests aws' } ] }, + { + name: 'test-auth-oidc-aws', + tags: ['latest', 'replica_set', 'oidc'], + commands: [ + updateExpansions({ + VERSION: 'latest', + TOPOLOGY: 'replica_set', + AUTH: 'auth', + ORCHESTRATION_FILE: 'auth-oidc.json' + }), + { func: 'install dependencies' }, + { func: 'bootstrap oidc' }, + { func: 'bootstrap mongo-orchestration' }, + { func: 'setup oidc roles' }, + { func: 'run oidc auth tests aws' } + ] + }, { name: 'test-socks5', tags: [], @@ -710,16 +727,49 @@ BUILD_VARIANTS.push({ tasks: ['test_azurekms_task_group', 'test-azurekms-fail-task'] }); -// TODO(DRIVERS-2416/NODE-4929) - Azure credentials are expired, a new drivers ticket -// should be created but at the moment for our test failures we will reference the -// open DRIVERS ticket and completed NODE ticket. -// BUILD_VARIANTS.push({ -// name: 'ubuntu20-test-azure-oidc', -// display_name: 'Azure OIDC', -// run_on: UBUNTU_20_OS, -// batchtime: 20160, -// tasks: ['testazureoidc_task_group'] -// }); +BUILD_VARIANTS.push({ + name: 'ubuntu20-test-azure-oidc', + display_name: 'Azure OIDC', + run_on: UBUNTU_20_OS, + expansions: { + NODE_LTS_VERSION: LATEST_LTS + }, + batchtime: 20160, + tasks: ['testazureoidc_task_group'] +}); + +BUILD_VARIANTS.push({ + name: 'ubuntu20-test-azure-oidc-auth', + display_name: 'Azure OIDC Unified Tests', + run_on: UBUNTU_20_OS, + expansions: { + NODE_LTS_VERSION: LATEST_LTS + }, + batchtime: 20160, + tasks: ['testazureoidcauth_task_group'] +}); + +BUILD_VARIANTS.push({ + name: 'ubuntu20-test-gcp-oidc', + display_name: 'GCP OIDC', + expansions: { + NODE_LTS_VERSION: LATEST_LTS + }, + run_on: UBUNTU_20_OS, + batchtime: 20160, + tasks: ['testgcpoidc_task_group'] +}); + +BUILD_VARIANTS.push({ + name: 'ubuntu20-test-gcp-oidc-auth', + display_name: 'GCP OIDC Unified Tests', + expansions: { + NODE_LTS_VERSION: LATEST_LTS + }, + run_on: UBUNTU_20_OS, + batchtime: 20160, + tasks: ['testgcpoidcauth_task_group'] +}); BUILD_VARIANTS.push({ name: 'rhel8-test-atlas', diff --git a/.evergreen/prepare-shell.sh b/.evergreen/prepare-shell.sh index f3f8ffad197..d47b4132569 100644 --- a/.evergreen/prepare-shell.sh +++ b/.evergreen/prepare-shell.sh @@ -31,7 +31,7 @@ export PATH="$MONGODB_BINARIES:$PATH" if [ ! -d "$DRIVERS_TOOLS" ]; then # Only clone driver tools if it does not exist - git clone --depth=1 "https://github.com/mongodb-labs/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" + git clone --depth=1 --branch DRIVERS-2601 "https://github.com/blink1073/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" fi echo "installed DRIVERS_EVERGREEN_TOOLS from commit $(git -C $DRIVERS_EVERGREEN_TOOLS rev-parse HEAD)" diff --git a/.evergreen/run-oidc-auth-tests.sh b/.evergreen/run-oidc-auth-tests.sh new file mode 100755 index 00000000000..56ca5f7c688 --- /dev/null +++ b/.evergreen/run-oidc-auth-tests.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -o errexit # Exit the script with error if any of the commands fail +set -o xtrace # Write all commands first to stderr + +PROVIDER_NAME=${PROVIDER_NAME:-"aws"} +PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."} +source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" + +MONGODB_URI=${MONGODB_URI:-"mongodb://127.0.0.1:27017"} + +export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} + +export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"} + +if [ "$PROVIDER_NAME" = "azure" ]; then + if [ -z "${AZUREOIDC_CLIENTID}" ]; then + echo "Must specify an AZUREOIDC_CLIENTID" + exit 1 + fi + + export UTIL_CLIENT_USER=$AZUREOIDC_USERNAME + export UTIL_CLIENT_PASSWORD="pwd123" + MONGODB_URI="${MONGODB_URI}/?authMechanism=MONGODB-OIDC" + MONGODB_URI="${MONGODB_URI}&authMechanismProperties=PROVIDER_NAME:azure" + MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api%3A%2F%2F${AZUREOIDC_CLIENTID}" + export MONGODB_URI="${MONGODB_URI},TOKEN_CLIENT_ID:${AZUREOIDC_TOKENCLIENT}" +else + if [ -z "${OIDC_TOKEN_DIR}" ]; then + echo "Must specify OIDC_TOKEN_DIR" + exit 1 + fi + + export UTIL_CLIENT_USER="bob" + export UTIL_CLIENT_PASSWORD="pwd123" + export MONGODB_URI="${MONGODB_URI}/test?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws" +fi + +npm run check:oidc-auth \ No newline at end of file diff --git a/.evergreen/run-oidc-tests-azure.sh b/.evergreen/run-oidc-tests-azure.sh index 6e65bff3f44..e8353620c10 100644 --- a/.evergreen/run-oidc-tests-azure.sh +++ b/.evergreen/run-oidc-tests-azure.sh @@ -4,8 +4,7 @@ set -o errexit # Exit the script with error if any of the commands fail export AZUREOIDC_DRIVERS_TAR_FILE=/tmp/node-mongodb-native.tgz tar czf $AZUREOIDC_DRIVERS_TAR_FILE . -export AZUREOIDC_TEST_CMD="source ./env.sh && PROVIDER_NAME=azure ./.evergreen/run-oidc-tests.sh" -export AZUREOIDC_CLIENTID=$AZUREOIDC_CLIENTID +export AZUREOIDC_TEST_CMD="source ./env.sh && PROVIDER_NAME=azure ./.evergreen/${SCRIPT}" export PROJECT_DIRECTORY=$PROJECT_DIRECTORY export PROVIDER_NAME=$PROVIDER_NAME bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh \ No newline at end of file diff --git a/.evergreen/run-oidc-tests-gcp.sh b/.evergreen/run-oidc-tests-gcp.sh new file mode 100644 index 00000000000..a34daf94c0a --- /dev/null +++ b/.evergreen/run-oidc-tests-gcp.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +export GCPOIDC_DRIVERS_TAR_FILE=/tmp/node-mongodb-native.tgz +tar czf $GCPOIDC_DRIVERS_TAR_FILE . +export GCPOIDC_TEST_CMD="source ./secrets-export.sh drivers/gcpoidc && PROVIDER_NAME=gcp ./.evergreen/${SCRIPT}" +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +export PROVIDER_NAME=$PROVIDER_NAME +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh \ No newline at end of file diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 98881a0c2d2..51ed1e37a11 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -12,24 +12,37 @@ export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} export MONGODB_URI=${MONGODB_URI:-"mongodb://localhost"} -if [ "$PROVIDER_NAME" = "aws" ]; then - export MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC" - export MONGODB_URI_MULTIPLE="${MONGODB_URI}:27018/?authMechanism=MONGODB-OIDC&directConnection=true" - - if [ -z "${OIDC_TOKEN_DIR}" ]; then - echo "Must specify OIDC_TOKEN_DIR" - exit 1 - fi - npm run check:oidc -elif [ "$PROVIDER_NAME" = "azure" ]; then +if [ "$PROVIDER_NAME" = "azure" ]; then if [ -z "${AZUREOIDC_CLIENTID}" ]; then echo "Must specify an AZUREOIDC_CLIENTID" exit 1 fi - MONGODB_URI="${MONGODB_URI}/?authMechanism=MONGODB-OIDC" + MONGODB_URI="mongodb://${AZUREOIDC_USERNAME}@127.0.0.1:27017/?authMechanism=MONGODB-OIDC" MONGODB_URI="${MONGODB_URI}&authMechanismProperties=PROVIDER_NAME:azure" export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api%3A%2F%2F${AZUREOIDC_CLIENTID}" npm run check:oidc-azure +elif [ "$PROVIDER_NAME" = "gcp" ]; then + if [ -z "${GCPOIDC_AUDIENCE}" ]; then + echo "Must specify an GCPOIDC_AUDIENCE" + exit 1 + fi + if [ -z "${GCPOIDC_ATLAS_URI}" ]; then + echo "Must specify an GCPOIDC_ATLAS_URI" + exit 1 + fi + MONGODB_URI="${GCPOIDC_ATLAS_URI}/?authMechanism=MONGODB-OIDC" + MONGODB_URI="${MONGODB_URI}&authMechanismProperties=PROVIDER_NAME:gcp" + export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:${GCPOIDC_AUDIENCE}" + npm run check:oidc-gcp else + echo $OIDC_ATLAS_URI_SINGLE + echo $OIDC_ATLAS_URI_MULTI + export MONGODB_URI_SINGLE=${OIDC_ATLAS_URI_SINGLE} + export MONGODB_URI_MULTI=${OIDC_ATLAS_URI_MULTI} + + if [ -z "${OIDC_TOKEN_DIR}" ]; then + echo "Must specify OIDC_TOKEN_DIR" + exit 1 + fi npm run check:oidc fi diff --git a/package.json b/package.json index 097a9f7b000..636f15ae06e 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,9 @@ "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", "check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts", + "check:oidc-auth": "mocha --config test/mocha_mongodb.json test/integration/auth/auth.spec.test.ts", "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.test.ts", + "check:oidc-gcp": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_gcp.prose.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.ts", diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index c086afb4e7e..8799a8fae5c 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -8,7 +8,7 @@ import { MongoMissingCredentialsError } from '../../error'; import { GSSAPICanonicalizationValue } from './gssapi'; -import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc'; +import type { OIDCCallbackFunction } from './mongodb_oidc'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers'; // https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst @@ -32,12 +32,13 @@ function getDefaultAuthMechanism(hello: Document | null): AuthMechanism { return AuthMechanism.MONGODB_CR; } -const ALLOWED_PROVIDER_NAMES: AuthMechanismProperties['PROVIDER_NAME'][] = ['aws', 'azure']; +const ALLOWED_PROVIDER_NAMES: AuthMechanismProperties['PROVIDER_NAME'][] = ['aws', 'azure', 'gcp']; const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'; /** @internal */ export const DEFAULT_ALLOWED_HOSTS = [ '*.mongodb.net', + '*.mongodb-qa.net', '*.mongodb-dev.net', '*.mongodbgov.net', 'localhost', @@ -47,7 +48,7 @@ export const DEFAULT_ALLOWED_HOSTS = [ /** Error for when the token audience is missing in the environment. */ const TOKEN_AUDIENCE_MISSING_ERROR = - 'TOKEN_AUDIENCE must be set in the auth mechanism properties when PROVIDER_NAME is azure.'; + 'TOKEN_AUDIENCE must be set in the auth mechanism properties when PROVIDER_NAME is azure or gcp.'; /** @public */ export interface AuthMechanismProperties extends Document { @@ -57,15 +58,17 @@ export interface AuthMechanismProperties extends Document { CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue; AWS_SESSION_TOKEN?: string; /** @experimental */ - REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction; + OIDC_CALLBACK?: OIDCCallbackFunction; /** @experimental */ - REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction; + OIDC_HUMAN_CALLBACK?: OIDCCallbackFunction; /** @experimental */ - PROVIDER_NAME?: 'aws' | 'azure'; + PROVIDER_NAME?: 'aws' | 'azure' | 'gcp'; /** @experimental */ ALLOWED_HOSTS?: string[]; /** @experimental */ TOKEN_AUDIENCE?: string; + /** @experimental */ + TOKEN_CLIENT_ID?: string; } /** @public */ @@ -179,14 +182,19 @@ export class MongoCredentials { } if (this.mechanism === AuthMechanism.MONGODB_OIDC) { - if (this.username && this.mechanismProperties.PROVIDER_NAME) { + if ( + this.username && + this.mechanismProperties.PROVIDER_NAME && + this.mechanismProperties.PROVIDER_NAME !== 'azure' + ) { throw new MongoInvalidArgumentError( - `username and PROVIDER_NAME may not be used together for mechanism '${this.mechanism}'.` + `username and PROVIDER_NAME '${this.mechanismProperties.PROVIDER_NAME}' may not be used together for mechanism '${this.mechanism}'.` ); } if ( - this.mechanismProperties.PROVIDER_NAME === 'azure' && + (this.mechanismProperties.PROVIDER_NAME === 'azure' || + this.mechanismProperties.PROVIDER_NAME === 'gcp') && !this.mechanismProperties.TOKEN_AUDIENCE ) { throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); @@ -203,21 +211,13 @@ export class MongoCredentials { ); } - if ( - this.mechanismProperties.REFRESH_TOKEN_CALLBACK && - !this.mechanismProperties.REQUEST_TOKEN_CALLBACK - ) { - throw new MongoInvalidArgumentError( - `A REQUEST_TOKEN_CALLBACK must be provided when using a REFRESH_TOKEN_CALLBACK for mechanism '${this.mechanism}'` - ); - } - if ( !this.mechanismProperties.PROVIDER_NAME && - !this.mechanismProperties.REQUEST_TOKEN_CALLBACK + !this.mechanismProperties.OIDC_CALLBACK && + !this.mechanismProperties.OIDC_HUMAN_CALLBACK ) { throw new MongoInvalidArgumentError( - `Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.` + `Either a PROVIDER_NAME, OIDC_CALLBACK, or OIDC_HUMAN_CALLBACK must be specified for mechanism '${this.mechanism}'.` ); } diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index f3584c4893e..2782adf319d 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -5,9 +5,11 @@ import type { HandshakeDocument } from '../connect'; import type { Connection } from '../connection'; import { type AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; -import { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; -import { AzureServiceWorkflow } from './mongodb_oidc/azure_service_workflow'; +import { AwsMachineWorkflow } from './mongodb_oidc/aws_machine_workflow'; +import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow'; import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; +import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow'; +import type { TokenCache } from './mongodb_oidc/token_cache'; /** Error when credentials are missing. */ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; @@ -16,7 +18,7 @@ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; * @public * @experimental */ -export interface IdPServerInfo { +export interface IdPInfo { issuer: string; clientId: string; requestScopes?: string[]; @@ -36,32 +38,30 @@ export interface IdPServerResponse { * @public * @experimental */ -export interface OIDCCallbackContext { +export interface OIDCResponse { + accessToken: string; + expiresInSeconds?: number; refreshToken?: string; - timeoutSeconds?: number; - timeoutContext?: AbortSignal; - version: number; } /** * @public * @experimental */ -export type OIDCRequestFunction = ( - info: IdPServerInfo, - context: OIDCCallbackContext -) => Promise; +export interface OIDCCallbackParams { + timeoutContext: AbortSignal; + version: number; + idpInfo?: IdPInfo; + refreshToken?: string; +} /** * @public * @experimental */ -export type OIDCRefreshFunction = ( - info: IdPServerInfo, - context: OIDCCallbackContext -) => Promise; +export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise; -type ProviderName = 'aws' | 'azure' | 'callback'; +type ProviderName = 'aws' | 'azure' | 'gcp' | 'callback'; export interface Workflow { /** @@ -71,10 +71,19 @@ export interface Workflow { execute( connection: Connection, credentials: MongoCredentials, - reauthenticating: boolean, + cache?: TokenCache, response?: Document ): Promise; + /** + * Each workflow should specify the correct custom behaviour for reauthentication. + */ + reauthenticate( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise; + /** * Get the document to add for speculative authentication. */ @@ -84,19 +93,23 @@ export interface Workflow { /** @internal */ export const OIDC_WORKFLOWS: Map = new Map(); OIDC_WORKFLOWS.set('callback', new CallbackWorkflow()); -OIDC_WORKFLOWS.set('aws', new AwsServiceWorkflow()); -OIDC_WORKFLOWS.set('azure', new AzureServiceWorkflow()); +OIDC_WORKFLOWS.set('aws', new AwsMachineWorkflow()); +OIDC_WORKFLOWS.set('azure', new AzureMachineWorkflow()); +OIDC_WORKFLOWS.set('gcp', new GCPMachineWorkflow()); /** * OIDC auth provider. * @experimental */ export class MongoDBOIDC extends AuthProvider { + cache?: TokenCache; + /** * Instantiate the auth provider. */ - constructor() { + constructor(cache?: TokenCache) { super(); + this.cache = cache; } /** @@ -106,7 +119,11 @@ export class MongoDBOIDC extends AuthProvider { const { connection, reauthenticating, response } = authContext; const credentials = getCredentials(authContext); const workflow = getWorkflow(credentials); - await workflow.execute(connection, credentials, reauthenticating, response); + if (reauthenticating) { + await workflow.reauthenticate(connection, credentials, this.cache); + } else { + await workflow.execute(connection, credentials, this.cache, response); + } } /** diff --git a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts b/src/cmap/auth/mongodb_oidc/aws_machine_workflow.ts similarity index 84% rename from src/cmap/auth/mongodb_oidc/aws_service_workflow.ts rename to src/cmap/auth/mongodb_oidc/aws_machine_workflow.ts index 5dd07b1d28e..138954b29af 100644 --- a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/aws_machine_workflow.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { MongoAWSError } from '../../../error'; -import { ServiceWorkflow } from './service_workflow'; +import { MachineWorkflow } from './machine_workflow'; /** Error for when the token is missing in the environment. */ const TOKEN_MISSING_ERROR = 'AWS_WEB_IDENTITY_TOKEN_FILE must be set in the environment.'; @@ -11,7 +11,7 @@ const TOKEN_MISSING_ERROR = 'AWS_WEB_IDENTITY_TOKEN_FILE must be set in the envi * * @internal */ -export class AwsServiceWorkflow extends ServiceWorkflow { +export class AwsMachineWorkflow extends MachineWorkflow { constructor() { super(); } diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts similarity index 64% rename from src/cmap/auth/mongodb_oidc/azure_service_workflow.ts rename to src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index fadbf5e9fd9..913b3f2c3cd 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,12 +1,10 @@ import { MongoAzureError } from '../../../error'; import { request } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; -import { AzureTokenCache } from './azure_token_cache'; -import { ServiceWorkflow } from './service_workflow'; +import { MachineWorkflow } from './machine_workflow'; /** Base URL for getting Azure tokens. */ -const AZURE_BASE_URL = - 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01'; +const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?'; /** Azure request headers. */ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); @@ -33,40 +31,38 @@ export interface AzureAccessToken { * * @internal */ -export class AzureServiceWorkflow extends ServiceWorkflow { - cache = new AzureTokenCache(); - +export class AzureMachineWorkflow extends MachineWorkflow { /** * Get the token from the environment. */ async getToken(credentials?: MongoCredentials): Promise { const tokenAudience = credentials?.mechanismProperties.TOKEN_AUDIENCE; + const username = credentials?.username; if (!tokenAudience) { throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); } - let token; - const entry = this.cache.getEntry(tokenAudience); - if (entry?.isValid()) { - token = entry.token; - } else { - this.cache.deleteEntry(tokenAudience); - const response = await getAzureTokenData(tokenAudience); - if (!isEndpointResultValid(response)) { - throw new MongoAzureError(ENDPOINT_RESULT_ERROR); - } - this.cache.addEntry(tokenAudience, response); - token = response.access_token; + const response = await getAzureTokenData(tokenAudience, username); + if (!isEndpointResultValid(response)) { + throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } - return token; + return response.access_token; } } /** * Hit the Azure endpoint to get the token data. */ -async function getAzureTokenData(tokenAudience: string): Promise { - const url = `${AZURE_BASE_URL}&resource=${tokenAudience}`; - const data = await request(url, { +async function getAzureTokenData( + tokenAudience: string, + username?: string +): Promise { + const url = new URL(AZURE_BASE_URL); + url.searchParams.append('api-version', '2018-02-01'); + url.searchParams.append('resource', tokenAudience); + if (username) { + url.searchParams.append('object_id', username); + } + const data = await request(url.toString(), { json: true, headers: AZURE_HEADERS }); diff --git a/src/cmap/auth/mongodb_oidc/azure_token_cache.ts b/src/cmap/auth/mongodb_oidc/azure_token_cache.ts deleted file mode 100644 index f68725120e8..00000000000 --- a/src/cmap/auth/mongodb_oidc/azure_token_cache.ts +++ /dev/null @@ -1,51 +0,0 @@ -import type { AzureAccessToken } from './azure_service_workflow'; -import { Cache, ExpiringCacheEntry } from './cache'; - -/** @internal */ -export class AzureTokenEntry extends ExpiringCacheEntry { - token: string; - - /** - * Instantiate the entry. - */ - constructor(token: string, expiration: number) { - super(expiration); - this.token = token; - } -} - -/** - * A cache of access tokens from Azure. - * @internal - */ -export class AzureTokenCache extends Cache { - /** - * Add an entry to the cache. - */ - addEntry(tokenAudience: string, token: AzureAccessToken): AzureTokenEntry { - const entry = new AzureTokenEntry(token.access_token, token.expires_in); - this.entries.set(tokenAudience, entry); - return entry; - } - - /** - * Create a cache key. - */ - cacheKey(tokenAudience: string): string { - return tokenAudience; - } - - /** - * Delete an entry from the cache. - */ - deleteEntry(tokenAudience: string): void { - this.entries.delete(tokenAudience); - } - - /** - * Get an Azure token entry from the cache. - */ - getEntry(tokenAudience: string): AzureTokenEntry | undefined { - return this.entries.get(tokenAudience); - } -} diff --git a/src/cmap/auth/mongodb_oidc/cache.ts b/src/cmap/auth/mongodb_oidc/cache.ts deleted file mode 100644 index e23685b3bca..00000000000 --- a/src/cmap/auth/mongodb_oidc/cache.ts +++ /dev/null @@ -1,63 +0,0 @@ -/* 5 minutes in milliseconds */ -const EXPIRATION_BUFFER_MS = 300000; - -/** - * An entry in a cache that can expire in a certain amount of time. - */ -export abstract class ExpiringCacheEntry { - expiration: number; - - /** - * Create a new expiring token entry. - */ - constructor(expiration: number) { - this.expiration = this.expirationTime(expiration); - } - /** - * The entry is still valid if the expiration is more than - * 5 minutes from the expiration time. - */ - isValid() { - return this.expiration - Date.now() > EXPIRATION_BUFFER_MS; - } - - /** - * Get an expiration time in milliseconds past epoch. - */ - private expirationTime(expiresInSeconds: number): number { - return Date.now() + expiresInSeconds * 1000; - } -} - -/** - * Base class for OIDC caches. - */ -export abstract class Cache { - entries: Map; - - /** - * Create a new cache. - */ - constructor() { - this.entries = new Map(); - } - - /** - * Clear the cache. - */ - clear() { - this.entries.clear(); - } - - /** - * Implement the cache key for the token. - */ - abstract cacheKey(address: string, username: string, callbackHash: string): string; - - /** - * Create a cache key from the address and username. - */ - hashedCacheKey(address: string, username: string, callbackHash: string): string { - return JSON.stringify([address, username, callbackHash]); - } -} diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts deleted file mode 100644 index b92a504b0a8..00000000000 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { MongoInvalidArgumentError } from '../../../error'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { - IdPServerInfo, - IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction -} from '../mongodb_oidc'; -import { Cache } from './cache'; - -/** Error message for when request callback is missing. */ -const REQUEST_CALLBACK_REQUIRED_ERROR = - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; -/* Counter for function "hashes".*/ -let FN_HASH_COUNTER = 0; -/* No function present function */ -const NO_FUNCTION: OIDCRequestFunction = async () => ({ accessToken: 'test' }); -/* The map of function hashes */ -const FN_HASHES = new WeakMap(); -/* Put the no function hash in the map. */ -FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); - -/** - * An entry of callbacks in the cache. - */ -interface CallbacksEntry { - requestCallback: OIDCRequestFunction; - refreshCallback?: OIDCRefreshFunction; - callbackHash: string; -} - -/** - * A cache of request and refresh callbacks per server/user. - */ -export class CallbackLockCache extends Cache { - /** - * Get the callbacks for the connection and credentials. If an entry does not - * exist a new one will get set. - */ - getEntry(connection: Connection, credentials: MongoCredentials): CallbacksEntry { - const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - if (!requestCallback) { - throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); - } - const callbackHash = hashFunctions(requestCallback, refreshCallback); - const key = this.cacheKey(connection.address, credentials.username, callbackHash); - const entry = this.entries.get(key); - if (entry) { - return entry; - } - return this.addEntry(key, callbackHash, requestCallback, refreshCallback); - } - - /** - * Set locked callbacks on for connection and credentials. - */ - private addEntry( - key: string, - callbackHash: string, - requestCallback: OIDCRequestFunction, - refreshCallback?: OIDCRefreshFunction - ): CallbacksEntry { - const entry = { - requestCallback: withLock(requestCallback), - refreshCallback: refreshCallback ? withLock(refreshCallback) : undefined, - callbackHash: callbackHash - }; - this.entries.set(key, entry); - return entry; - } - - /** - * Create a cache key from the address and username. - */ - cacheKey(address: string, username: string, callbackHash: string): string { - return this.hashedCacheKey(address, username, callbackHash); - } -} - -/** - * Ensure the callback is only executed one at a time. - */ -function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { - let lock: Promise = Promise.resolve(); - return async (info: IdPServerInfo, context: OIDCCallbackContext): Promise => { - await lock; - lock = lock.then(() => callback(info, context)); - return lock; - }; -} - -/** - * Get the hash string for the request and refresh functions. - */ -function hashFunctions(requestFn: OIDCRequestFunction, refreshFn?: OIDCRefreshFunction): string { - let requestHash = FN_HASHES.get(requestFn); - let refreshHash = FN_HASHES.get(refreshFn ?? NO_FUNCTION); - if (requestHash == null) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - requestHash = FN_HASH_COUNTER; - FN_HASHES.set(requestFn, FN_HASH_COUNTER); - } - if (refreshHash == null && refreshFn) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - refreshHash = FN_HASH_COUNTER; - FN_HASHES.set(refreshFn, FN_HASH_COUNTER); - } - return `${requestHash}-${refreshHash}`; -} diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 9822fd1e505..865aa2b1d79 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,26 +1,24 @@ -import { Binary, BSON, type Document } from 'bson'; +import { BSON, type Document } from 'bson'; -import { MONGODB_ERROR_CODES, MongoError, MongoMissingCredentialsError } from '../../../error'; +import { MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; +import type { AuthMechanismProperties, MongoCredentials } from '../mongo_credentials'; import type { - IdPServerInfo, + IdPInfo, IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction, + OIDCCallbackFunction, + OIDCCallbackParams, Workflow } from '../mongodb_oidc'; -import { AuthMechanism } from '../providers'; -import { CallbackLockCache } from './callback_lock_cache'; -import { TokenEntryCache } from './token_entry_cache'; +import { finishCommandDocument, startCommandDocument } from './command_builders'; +import { type TokenCache } from './token_cache'; /** The current version of OIDC implementation. */ -const OIDC_VERSION = 0; +const OIDC_VERSION = 1; -/** 5 minutes in seconds */ -const TIMEOUT_S = 300; +/** 5 minutes in milliseconds */ +const HUMAN_TIMEOUT_MS = 300000; /** Properties allowed on results of callbacks. */ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; @@ -29,22 +27,21 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; const CALLBACK_RESULT_ERROR = 'User provided OIDC callbacks must return a valid object with an accessToken.'; +const NO_CALLBACK = 'No OIDC_CALLBACK or OIDC_HUMAN_CALLBACK provided for callback workflow.'; + +/** + * The OIDC callback information. + */ +interface OIDCCallbackInfo { + callback: OIDCCallbackFunction; + isHumanWorkflow: boolean; +} + /** * OIDC implementation of a callback based workflow. * @internal */ export class CallbackWorkflow implements Workflow { - cache: TokenEntryCache; - callbackCache: CallbackLockCache; - - /** - * Instantiate the workflow - */ - constructor() { - this.cache = new TokenEntryCache(); - this.callbackCache = new CallbackLockCache(); - } - /** * Get the document to add for speculative authentication. This also needs * to add a db field from the credentials source. @@ -55,96 +52,55 @@ export class CallbackWorkflow implements Workflow { return { speculativeAuthenticate: document }; } + /** + * Reauthenticate the callback workflow. + * For reauthentication: + * - Check if the connection's accessToken is not equal to the token manager's. + * - If they are different, use the token from the manager and set it on the connection and finish auth. + * - On success return, on error continue. + * - start auth to update the IDP information + * - If the idp info has changed, clear access token and refresh token. + * - If the idp info has not changed, attempt to use the refresh token. + * - if there's still a refresh token at this point, attempt to finish auth with that. + * - Attempt the full auth run, on error, raise to user. + */ + async reauthenticate( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + // Reauthentication should always remove the access token. + cache?.remove(); + return this.execute(connection, credentials, cache); + } + /** * Execute the OIDC callback workflow. */ async execute( connection: Connection, credentials: MongoCredentials, - reauthenticating: boolean, + cache?: TokenCache, response?: Document ): Promise { - // Get the callbacks with locks from the callback lock cache. - const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getEntry( - connection, - credentials - ); - // Look for an existing entry in the cache. - const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); - let result; - if (entry) { - // Reauthentication cannot use a token from the cache since the server has - // stated it is invalid by the request for reauthentication. - if (entry.isValid() && !reauthenticating) { - // Presence of a valid cache entry means we can skip to the finishing step. - result = await this.finishAuthentication( - connection, - credentials, - entry.tokenResult, - response?.speculativeAuthenticate?.conversationId - ); - } else { - // Presence of an expired cache entry means we must fetch a new one and - // then execute the final step. - const tokenResult = await this.fetchAccessToken( - connection, - credentials, - entry.serverInfo, - reauthenticating, - callbackHash, - requestCallback, - refreshCallback - ); - try { - result = await this.finishAuthentication( - connection, - credentials, - tokenResult, - reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId - ); - } catch (error) { - // If we are reauthenticating and this errors with reauthentication - // required, we need to do the entire process over again and clear - // the cache entry. - if ( - reauthenticating && - error instanceof MongoError && - error.code === MONGODB_ERROR_CODES.Reauthenticate - ) { - this.cache.deleteEntry(connection.address, credentials.username, callbackHash); - result = await this.execute(connection, credentials, reauthenticating); - } else { - throw error; - } - } - } + const callbackInfo = getCallback(credentials.mechanismProperties); + const startDocument = await this.startAuthentication(connection, credentials, response); + const conversationId = startDocument.conversationId; + const idpInfo = BSON.deserialize(startDocument.payload.buffer) as IdPInfo; + // If we are not reauthenticating we can use the token from the cache. + let tokenResult: IdPServerResponse; + if (cache?.hasToken()) { + tokenResult = cache.get(); } else { - // No entry in the cache requires us to do all authentication steps - // from start to finish, including getting a fresh token for the cache. - const startDocument = await this.startAuthentication( - connection, - credentials, - reauthenticating, - response - ); - const conversationId = startDocument.conversationId; - const serverResult = BSON.deserialize(startDocument.payload.buffer) as IdPServerInfo; - const tokenResult = await this.fetchAccessToken( - connection, - credentials, - serverResult, - reauthenticating, - callbackHash, - requestCallback, - refreshCallback - ); - result = await this.finishAuthentication( - connection, - credentials, - tokenResult, - conversationId - ); + tokenResult = await this.fetchAccessToken(connection, credentials, idpInfo, callbackInfo); + cache?.put(tokenResult); } + const result = await this.finishAuthentication( + connection, + credentials, + tokenResult, + conversationId + ); return result; } @@ -156,11 +112,10 @@ export class CallbackWorkflow implements Workflow { private async startAuthentication( connection: Connection, credentials: MongoCredentials, - reauthenticating: boolean, response?: Document ): Promise { let result; - if (!reauthenticating && response?.speculativeAuthenticate) { + if (response?.speculativeAuthenticate) { result = response.speculativeAuthenticate; } else { result = await connection.command( @@ -196,76 +151,39 @@ export class CallbackWorkflow implements Workflow { private async fetchAccessToken( connection: Connection, credentials: MongoCredentials, - serverInfo: IdPServerInfo, - reauthenticating: boolean, - callbackHash: string, - requestCallback: OIDCRequestFunction, - refreshCallback?: OIDCRefreshFunction + idpInfo: IdPInfo, + callbackInfo: OIDCCallbackInfo ): Promise { - // Get the token from the cache. - const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); - let result; - const context: OIDCCallbackContext = { timeoutSeconds: TIMEOUT_S, version: OIDC_VERSION }; - // Check if there's a token in the cache. - if (entry) { - // If the cache entry is valid, return the token result. - if (entry.isValid() && !reauthenticating) { - return entry.tokenResult; - } - // If the cache entry is not valid, remove it from the cache and first attempt - // to use the refresh callback to get a new token. If no refresh callback - // exists, then fallback to the request callback. - if (refreshCallback) { - context.refreshToken = entry.tokenResult.refreshToken; - result = await refreshCallback(serverInfo, context); - } else { - result = await requestCallback(serverInfo, context); - } - } else { - // With no token in the cache we use the request callback. - result = await requestCallback(serverInfo, context); - } + const params: OIDCCallbackParams = { + timeoutContext: AbortSignal.timeout( + callbackInfo.isHumanWorkflow ? HUMAN_TIMEOUT_MS : HUMAN_TIMEOUT_MS + ), // TODO: CSOT + version: OIDC_VERSION, + idpInfo: idpInfo // TODO: refreshToken? + }; + // With no token in the cache we use the request callback. + const result = await callbackInfo.callback(params); // Validate that the result returned by the callback is acceptable. If it is not // we must clear the token result from the cache. if (isCallbackResultInvalid(result)) { - this.cache.deleteEntry(connection.address, credentials.username, callbackHash); throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } - // Cleanup the cache. - this.cache.deleteExpiredEntries(); - // Put the new entry into the cache. - this.cache.addEntry( - connection.address, - credentials.username || '', - callbackHash, - result, - serverInfo - ); return result; } } /** - * Generate the finishing command document for authentication. Will be a - * saslStart or saslContinue depending on the presence of a conversation id. + * Returns a callback, either machine or human, and a flag whether the workflow is + * human or not. */ -function finishCommandDocument(token: string, conversationId?: number): Document { - if (conversationId != null && typeof conversationId === 'number') { - return { - saslContinue: 1, - conversationId: conversationId, - payload: new Binary(BSON.serialize({ jwt: token })) - }; +function getCallback(mechanismProperties: AuthMechanismProperties): OIDCCallbackInfo { + if (!mechanismProperties.OIDC_CALLBACK || !mechanismProperties.OIDC_HUMAN_CALLBACK) { + throw new MongoMissingCredentialsError(NO_CALLBACK); } - // saslContinue requires a conversationId in the command to be valid so in this - // case the server allows "step two" to actually be a saslStart with the token - // as the jwt since the use of the cached value has no correlating conversating - // on the particular connection. - return { - saslStart: 1, - mechanism: AuthMechanism.MONGODB_OIDC, - payload: new Binary(BSON.serialize({ jwt: token })) - }; + if (mechanismProperties.OIDC_CALLBACK) { + return { callback: mechanismProperties.OIDC_CALLBACK, isHumanWorkflow: false }; + } + return { callback: mechanismProperties.OIDC_HUMAN_CALLBACK, isHumanWorkflow: true }; } /** @@ -278,19 +196,3 @@ function isCallbackResultInvalid(tokenResult: unknown): boolean { if (!('accessToken' in tokenResult)) return true; return !Object.getOwnPropertyNames(tokenResult).every(prop => RESULT_PROPERTIES.includes(prop)); } - -/** - * Generate the saslStart command document. - */ -function startCommandDocument(credentials: MongoCredentials): Document { - const payload: Document = {}; - if (credentials.username) { - payload.n = credentials.username; - } - return { - saslStart: 1, - autoAuthorize: 1, - mechanism: AuthMechanism.MONGODB_OIDC, - payload: new Binary(BSON.serialize(payload)) - }; -} diff --git a/src/cmap/auth/mongodb_oidc/command_builders.ts b/src/cmap/auth/mongodb_oidc/command_builders.ts new file mode 100644 index 00000000000..ee6284343f3 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/command_builders.ts @@ -0,0 +1,43 @@ +import { Binary, BSON, type Document } from 'bson'; + +import { type MongoCredentials } from '../mongo_credentials'; +import { AuthMechanism } from '../providers'; + +/** + * Generate the finishing command document for authentication. Will be a + * saslStart or saslContinue depending on the presence of a conversation id. + */ +export function finishCommandDocument(token: string, conversationId?: number): Document { + if (conversationId != null && typeof conversationId === 'number') { + return { + saslContinue: 1, + conversationId: conversationId, + payload: new Binary(BSON.serialize({ jwt: token })) + }; + } + // saslContinue requires a conversationId in the command to be valid so in this + // case the server allows "step two" to actually be a saslStart with the token + // as the jwt since the use of the cached value has no correlating conversating + // on the particular connection. + return { + saslStart: 1, + mechanism: AuthMechanism.MONGODB_OIDC, + payload: new Binary(BSON.serialize({ jwt: token })) + }; +} + +/** + * Generate the saslStart command document. + */ +export function startCommandDocument(credentials: MongoCredentials): Document { + const payload: Document = {}; + if (credentials.username) { + payload.n = credentials.username; + } + return { + saslStart: 1, + autoAuthorize: 1, + mechanism: AuthMechanism.MONGODB_OIDC, + payload: new Binary(BSON.serialize(payload)) + }; +} diff --git a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts new file mode 100644 index 00000000000..74992635944 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -0,0 +1,48 @@ +import { MongoGCPError } from '../../../error'; +import { request } from '../../../utils'; +import { type MongoCredentials } from '../mongo_credentials'; +import { MachineWorkflow } from './machine_workflow'; + +/** GCP base URL. */ +const GCP_BASE_URL = + 'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity'; + +/** GCP request headers. */ +const GCP_HEADERS = Object.freeze({ 'Metadata-Flavor': 'Google' }); + +/** Error for when the token audience is missing in the environment. */ +const TOKEN_AUDIENCE_MISSING_ERROR = + 'TOKEN_AUDIENCE must be set in the auth mechanism properties when PROVIDER_NAME is gcp.'; + +/** + * The GCP access token format. + * @internal + */ +export interface GCPAccessToken { + access_token: string; +} + +export class GCPMachineWorkflow extends MachineWorkflow { + /** + * Get the token from the environment. + */ + async getToken(credentials?: MongoCredentials): Promise { + const tokenAudience = credentials?.mechanismProperties.TOKEN_AUDIENCE; + if (!tokenAudience) { + throw new MongoGCPError(TOKEN_AUDIENCE_MISSING_ERROR); + } + return getGcpTokenData(tokenAudience); + } +} + +/** + * Hit the GCP endpoint to get the token data. + */ +async function getGcpTokenData(tokenAudience: string): Promise { + const url = new URL(GCP_BASE_URL); + url.searchParams.append('audience', tokenAudience); + return request(url.toString(), { + json: false, + headers: GCP_HEADERS + }); +} diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts new file mode 100644 index 00000000000..721042dc682 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -0,0 +1,73 @@ +import { type Document } from 'bson'; + +import { ns } from '../../../utils'; +import type { Connection } from '../../connection'; +import type { MongoCredentials } from '../mongo_credentials'; +import type { Workflow } from '../mongodb_oidc'; +import { finishCommandDocument } from './command_builders'; +import { type TokenCache } from './token_cache'; + +/** + * Common behaviour for OIDC machine workflows. + * @internal + */ +export abstract class MachineWorkflow implements Workflow { + /** + * Execute the workflow. Gets the token from the subclass implementation. + */ + async execute( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + const token = await this.getTokenFromCacheOrEnv(credentials, cache); + const command = finishCommandDocument(token); + return connection.command(ns(credentials.source), command, undefined); + } + + /** + * Reauthenticate on a machine workflow just grabs the token again since the server + * has said the current access token is invalid or expired. + */ + async reauthenticate( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + // Reauthentication implies the token has expired. + cache?.remove(); + return this.execute(connection, credentials, cache); + } + + /** + * Get the document to add for speculative authentication. + */ + async speculativeAuth(credentials: MongoCredentials, cache?: TokenCache): Promise { + const token = await this.getTokenFromCacheOrEnv(credentials, cache); + const document = finishCommandDocument(token); + document.db = credentials.source; + return { speculativeAuthenticate: document }; + } + + /** + * Get the token from the cache or environment. + */ + private async getTokenFromCacheOrEnv( + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + let token; + if (cache?.hasToken()) { + token = cache.get().accessToken; + } else { + token = await this.getToken(credentials); + cache?.put({ accessToken: token }); + } + return token; + } + + /** + * Get the token from the environment or endpoint. + */ + abstract getToken(credentials: MongoCredentials): Promise; +} diff --git a/src/cmap/auth/mongodb_oidc/service_workflow.ts b/src/cmap/auth/mongodb_oidc/service_workflow.ts deleted file mode 100644 index afea78fad53..00000000000 --- a/src/cmap/auth/mongodb_oidc/service_workflow.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BSON, type Document } from 'bson'; - -import { ns } from '../../../utils'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { Workflow } from '../mongodb_oidc'; -import { AuthMechanism } from '../providers'; - -/** - * Common behaviour for OIDC device workflows. - * @internal - */ -export abstract class ServiceWorkflow implements Workflow { - /** - * Execute the workflow. Looks for AWS_WEB_IDENTITY_TOKEN_FILE in the environment - * and then attempts to read the token from that path. - */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { - const token = await this.getToken(credentials); - const command = commandDocument(token); - return connection.command(ns(credentials.source), command, undefined); - } - - /** - * Get the document to add for speculative authentication. - */ - async speculativeAuth(credentials: MongoCredentials): Promise { - const token = await this.getToken(credentials); - const document = commandDocument(token); - document.db = credentials.source; - return { speculativeAuthenticate: document }; - } - - /** - * Get the token from the environment or endpoint. - */ - abstract getToken(credentials: MongoCredentials): Promise; -} - -/** - * Create the saslStart command document. - */ -export function commandDocument(token: string): Document { - return { - saslStart: 1, - mechanism: AuthMechanism.MONGODB_OIDC, - payload: BSON.serialize({ jwt: token }) - }; -} diff --git a/src/cmap/auth/mongodb_oidc/token_cache.ts b/src/cmap/auth/mongodb_oidc/token_cache.ts new file mode 100644 index 00000000000..fb711c25c8a --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/token_cache.ts @@ -0,0 +1,26 @@ +import { MongoDriverError } from '../../../error'; +import { type IdPServerResponse } from '../mongodb_oidc'; + +/** @internal */ +export class TokenCache { + private tokenResult?: IdPServerResponse; + + hasToken(): boolean { + return !!this.tokenResult; + } + + get(): IdPServerResponse { + if (!this.tokenResult) { + throw new MongoDriverError('no token'); + } + return this.tokenResult; + } + + put(result: IdPServerResponse) { + this.tokenResult = result; + } + + remove() { + this.tokenResult = undefined; + } +} diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts deleted file mode 100644 index 1b5b9de3314..00000000000 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ /dev/null @@ -1,77 +0,0 @@ -import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc'; -import { Cache, ExpiringCacheEntry } from './cache'; - -/* Default expiration is now for when no expiration provided */ -const DEFAULT_EXPIRATION_SECS = 0; - -/** @internal */ -export class TokenEntry extends ExpiringCacheEntry { - tokenResult: IdPServerResponse; - serverInfo: IdPServerInfo; - - /** - * Instantiate the entry. - */ - constructor(tokenResult: IdPServerResponse, serverInfo: IdPServerInfo, expiration: number) { - super(expiration); - this.tokenResult = tokenResult; - this.serverInfo = serverInfo; - } -} - -/** - * Cache of OIDC token entries. - * @internal - */ -export class TokenEntryCache extends Cache { - /** - * Set an entry in the token cache. - */ - addEntry( - address: string, - username: string, - callbackHash: string, - tokenResult: IdPServerResponse, - serverInfo: IdPServerInfo - ): TokenEntry { - const entry = new TokenEntry( - tokenResult, - serverInfo, - tokenResult.expiresInSeconds ?? DEFAULT_EXPIRATION_SECS - ); - this.entries.set(this.cacheKey(address, username, callbackHash), entry); - return entry; - } - - /** - * Delete an entry from the cache. - */ - deleteEntry(address: string, username: string, callbackHash: string): void { - this.entries.delete(this.cacheKey(address, username, callbackHash)); - } - - /** - * Get an entry from the cache. - */ - getEntry(address: string, username: string, callbackHash: string): TokenEntry | undefined { - return this.entries.get(this.cacheKey(address, username, callbackHash)); - } - - /** - * Delete all expired entries from the cache. - */ - deleteExpiredEntries(): void { - for (const [key, entry] of this.entries) { - if (!entry.isValid()) { - this.entries.delete(key); - } - } - } - - /** - * Create a cache key from the address and username. - */ - cacheKey(address: string, username: string, callbackHash: string): string { - return this.hashedCacheKey(address, username, callbackHash); - } -} diff --git a/src/error.ts b/src/error.ts index b488d0d5d75..f374221f39a 100644 --- a/src/error.ts +++ b/src/error.ts @@ -547,6 +547,34 @@ export class MongoAzureError extends MongoRuntimeError { } } +/** + * A error generated when the user attempts to authenticate + * via GCP, but fails. + * + * @public + * @category Error + */ +export class MongoGCPError extends MongoRuntimeError { + /** + * **Do not use this constructor!** + * + * Meant for internal use only. + * + * @remarks + * This class is only meant to be constructed within the driver. This constructor is + * not subject to semantic versioning compatibility guarantees and may change at any time. + * + * @public + **/ + constructor(message: string) { + super(message); + } + + override get name(): string { + return 'MongoGCPError'; + } +} + /** * An error generated when a ChangeStream operation fails to execute. * diff --git a/src/index.ts b/src/index.ts index aae568dd79e..bded4afaa72 100644 --- a/src/index.ts +++ b/src/index.ts @@ -52,6 +52,7 @@ export { MongoDriverError, MongoError, MongoExpiredSessionError, + MongoGCPError, MongoGridFSChunkError, MongoGridFSStreamError, MongoInvalidArgumentError, @@ -99,6 +100,7 @@ export { export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; +export { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export { AuthMechanism } from './cmap/auth/providers'; export { Compressor } from './cmap/wire_protocol/compression'; export { CURSOR_FLAGS } from './cursor/abstract_cursor'; @@ -250,11 +252,11 @@ export type { MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; export type { - IdPServerInfo, + IdPInfo, IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction + OIDCCallbackFunction, + OIDCCallbackParams, + OIDCResponse } from './cmap/auth/mongodb_oidc'; export type { MessageHeader, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index be039944a4f..71422880b11 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -11,6 +11,7 @@ import { DEFAULT_ALLOWED_HOSTS, type MongoCredentials } from './cmap/auth/mongo_credentials'; +import { type TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; import { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; @@ -828,6 +829,8 @@ export interface MongoOptions metadata: ClientMetadata; /** @internal */ autoEncrypter?: AutoEncrypter; + /** @internal */ + tokenCache?: TokenCache; proxyHost?: string; proxyPort?: number; proxyUsername?: string; diff --git a/src/mongo_client_auth_providers.ts b/src/mongo_client_auth_providers.ts index 557783c4e17..f3444faf162 100644 --- a/src/mongo_client_auth_providers.ts +++ b/src/mongo_client_auth_providers.ts @@ -3,6 +3,7 @@ import { GSSAPI } from './cmap/auth/gssapi'; import { MongoCR } from './cmap/auth/mongocr'; import { MongoDBAWS } from './cmap/auth/mongodb_aws'; import { MongoDBOIDC } from './cmap/auth/mongodb_oidc'; +import { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; import { Plain } from './cmap/auth/plain'; import { AuthMechanism } from './cmap/auth/providers'; import { ScramSHA1, ScramSHA256 } from './cmap/auth/scram'; @@ -14,7 +15,7 @@ const AUTH_PROVIDERS = new Map AuthProvider>([ [AuthMechanism.MONGODB_AWS, () => new MongoDBAWS()], [AuthMechanism.MONGODB_CR, () => new MongoCR()], [AuthMechanism.MONGODB_GSSAPI, () => new GSSAPI()], - [AuthMechanism.MONGODB_OIDC, () => new MongoDBOIDC()], + [AuthMechanism.MONGODB_OIDC, () => new MongoDBOIDC(new TokenCache())], [AuthMechanism.MONGODB_PLAIN, () => new Plain()], [AuthMechanism.MONGODB_SCRAM_SHA1, () => new ScramSHA1()], [AuthMechanism.MONGODB_SCRAM_SHA256, () => new ScramSHA256()], diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts index 2dc95b4c935..58338701f4f 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -5,14 +5,10 @@ import { type CommandFailedEvent, type CommandStartedEvent, type CommandSucceededEvent, - type MongoClient, - OIDC_WORKFLOWS + type MongoClient } from '../../mongodb'; -describe('OIDC Auth Spec Prose Tests', function () { - const callbackCache = OIDC_WORKFLOWS.get('callback').cache; - const azureCache = OIDC_WORKFLOWS.get('azure').cache; - +describe('OIDC Auth Spec Azure Prose Tests', function () { describe('3. Azure Automatic Auth', function () { let client: MongoClient; let collection: Collection; @@ -39,7 +35,7 @@ describe('OIDC Auth Spec Prose Tests', function () { // Close the client. it('successfully authenticates', async function () { const result = await collection.findOne(); - expect(result).to.be.null; + expect(result).to.not.be.null; }); }); @@ -59,13 +55,12 @@ describe('OIDC Auth Spec Prose Tests', function () { // Close the client. it('successfully authenticates', async function () { const result = await collection.findOne(); - expect(result).to.be.null; + expect(result).to.not.be.null; }); }); describe('3.3 Main Cache Not Used', function () { beforeEach(function () { - callbackCache?.clear(); client = this.configuration.newClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -77,15 +72,12 @@ describe('OIDC Auth Spec Prose Tests', function () { // Assert that the main OIDC cache is empty. it('does not use the main callback cache', async function () { const result = await collection.findOne(); - expect(result).to.be.null; - expect(callbackCache.entries).to.be.empty; + expect(result).to.not.be.null; }); }); describe('3.4 Azure Cache is Used', function () { beforeEach(function () { - callbackCache?.clear(); - azureCache?.clear(); client = this.configuration.newClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -97,9 +89,7 @@ describe('OIDC Auth Spec Prose Tests', function () { // Assert that the Azure OIDC cache has one entry. it('uses the Azure OIDC cache', async function () { const result = await collection.findOne(); - expect(result).to.be.null; - expect(callbackCache.entries).to.be.empty; - expect(azureCache.entries.size).to.equal(1); + expect(result).to.not.be.null; }); }); @@ -156,7 +146,6 @@ describe('OIDC Auth Spec Prose Tests', function () { }; beforeEach(async function () { - azureCache?.clear(); client = this.configuration.newClient(process.env.MONGODB_URI, { monitorCommands: true }); await client.db('test').collection('test').findOne(); addListeners(); diff --git a/test/integration/auth/mongodb_oidc_gcp.prose.test.ts b/test/integration/auth/mongodb_oidc_gcp.prose.test.ts new file mode 100644 index 00000000000..d815fc04a51 --- /dev/null +++ b/test/integration/auth/mongodb_oidc_gcp.prose.test.ts @@ -0,0 +1,35 @@ +import { expect } from 'chai'; + +import { type Collection, type MongoClient } from '../../mongodb'; + +describe('OIDC Auth Spec GCP Prose Tests', function () { + describe('3. GCP Automatic Auth', function () { + let client: MongoClient; + let collection: Collection; + + beforeEach(function () { + if (!this.configuration.isGcpOIDC(process.env.MONGODB_URI)) { + this.skipReason = 'GCP OIDC prose tests require an Azure OIDC environment.'; + this.skip(); + } + }); + + afterEach(async function () { + await client?.close(); + }); + + describe('3.1 Connect', function () { + beforeEach(function () { + client = this.configuration.newClient(process.env.MONGODB_URI); + collection = client.db('test').collection('test'); + }); + + // Assert that a find operation succeeds. + // Close the client. + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.not.be.null; + }); + }); + }); +}); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index bb4cfcb671f..9932d79104b 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -9,15 +9,13 @@ import { type CommandFailedEvent, type CommandStartedEvent, type CommandSucceededEvent, - type IdPServerInfo, MongoClient, MongoInvalidArgumentError, MongoMissingCredentialsError, MongoServerError, - OIDC_WORKFLOWS, - type OIDCCallbackContext + type OIDCCallbackParams, + type OIDCResponse } from '../mongodb'; -import { sleep } from '../tools/utils'; describe('MONGODB-OIDC', function () { context('when running in the environment', function () { @@ -27,48 +25,25 @@ describe('MONGODB-OIDC', function () { }); describe('OIDC Auth Spec Prose Tests', function () { - // Set up the cache variable. - const cache = OIDC_WORKFLOWS.get('callback').cache; - const callbackCache = OIDC_WORKFLOWS.get('callback').callbackCache; // Creates a request function for use in the test. const createRequestCallback = ( username = 'test_user1', expiresInSeconds?: number, extraFields?: any ) => { - return async (info: IdPServerInfo, context: OIDCCallbackContext) => { + return async (params: OIDCCallbackParams) => { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { encoding: 'utf8' }); // Do some basic property assertions. - expect(context).to.have.property('timeoutSeconds'); - expect(info).to.have.property('issuer'); - expect(info).to.have.property('clientId'); - return generateResult(token, expiresInSeconds, extraFields); - }; - }; - - // Creates a refresh function for use in the test. - const createRefreshCallback = ( - username = 'test_user1', - expiresInSeconds?: number, - extraFields?: any - ) => { - return async (info: IdPServerInfo, context: OIDCCallbackContext) => { - const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { - encoding: 'utf8' - }); - // Do some basic property assertions. - expect(context).to.have.property('timeoutSeconds'); - expect(info).to.have.property('issuer'); - expect(info).to.have.property('clientId'); + expect(params).to.have.property('timeoutContext'); return generateResult(token, expiresInSeconds, extraFields); }; }; // Generates the result the request or refresh callback returns. const generateResult = (token: string, expiresInSeconds?: number, extraFields?: any) => { - const response: OIDCRequestTokenResult = { accessToken: token }; + const response: OIDCResponse = { accessToken: token }; if (expiresInSeconds) { response.expiresInSeconds = expiresInSeconds; } @@ -78,36 +53,25 @@ describe('MONGODB-OIDC', function () { return response; }; - beforeEach(function () { - callbackCache.clear(); - }); - describe('1. Callback-Driven Auth', function () { let client: MongoClient; let collection: Collection; - beforeEach(function () { - cache.clear(); - }); - afterEach(async function () { await client?.close(); }); describe('1.1 Single Principal Implicit Username', function () { before(function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create the default OIDC client. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() + OIDC_TOKEN_CALLBACK: createRequestCallback() } }); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a request callback returns a valid token. - // Create a client that uses the default OIDC url and the request callback. - // Perform a find operation. that succeeds. // Close the client. it('successfully authenticates', async function () { const result = await collection.findOne(); @@ -117,17 +81,18 @@ describe('MONGODB-OIDC', function () { describe('1.2 Single Principal Explicit Username', function () { before(function () { - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { + // Create a client with ``MONGODB_URI_SINGLE``, a username of ``test_user1``, and the OIDC request callback. + const url = new URL(process.env.MONGODB_URI_SINGLE); + url.username = 'test_user1'; + url.searchParams.set('authMechanism', 'MONGODB-OIDC'); + client = new MongoClient(url.toString(), { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() + OIDC_TOKEN_CALLBACK: createRequestCallback() } }); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a request callback that returns a valid token. - // Create a client with a url of the form mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC and the OIDC request callback. // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { @@ -138,20 +103,18 @@ describe('MONGODB-OIDC', function () { describe('1.3 Multiple Principal User 1', function () { before(function () { - client = new MongoClient( - 'mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', - { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() - } + // Create a client with ``MONGODB_URI_MULTI``, a username of ``test_user1``, and the OIDC request callback. + const url = new URL(process.env.MONGODB_URI_MULTI); + url.username = 'test_user1'; + url.searchParams.set('authMechanism', 'MONGODB-OIDC'); + client = new MongoClient(url.toString(), { + authMechanismProperties: { + OIDC_TOKEN_CALLBACK: createRequestCallback() } - ); - collection = client.db('test').collection('test'); + }); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a request callback that returns a valid token. - // Create a client with a url of the form mongodb://test_user1@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { @@ -162,20 +125,18 @@ describe('MONGODB-OIDC', function () { describe('1.4 Multiple Principal User 2', function () { before(function () { - client = new MongoClient( - 'mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', - { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user2') - } + // Create a client with ``MONGODB_URI_MULTI``, a username of ``test_user2``, and the OIDC request callback. + const url = new URL(process.env.MONGODB_URI_MULTI); + url.username = 'test_user2'; + url.searchParams.set('authMechanism', 'MONGODB-OIDC'); + client = new MongoClient(url.toString(), { + authMechanismProperties: { + OIDC_TOKEN_CALLBACK: createRequestCallback('test_user2') } - ); - collection = client.db('test').collection('test'); + }); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a request callback that reads in the generated test_user2 token file. - // Create a client with a url of the form mongodb://test_user2@localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { @@ -186,19 +147,15 @@ describe('MONGODB-OIDC', function () { describe('1.5 Multiple Principal No User', function () { before(function () { - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred', - { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() - } + // Create a client with ``MONGODB_URI_MULTI``, no username, and the OIDC request callback. + client = new MongoClient(`${process.env.MONGODB_URI_MULTI}?authMechanism=MONGODB-OIDC`, { + authMechanismProperties: { + OIDC_TOKEN_CALLBACK: createRequestCallback() } - ); - collection = client.db('test').collection('test'); + }); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&directConnection=true&readPreference=secondaryPreferred and a valid OIDC request callback. // Assert that a find operation fails. // Close the client. it('fails authentication', async function () { @@ -213,26 +170,22 @@ describe('MONGODB-OIDC', function () { }); describe('1.6 Allowed Hosts Blocked', function () { - before(function () { - cache.clear(); - }); - - // Clear the cache. - // Create a client that uses the OIDC url and a request callback, and an - // ``ALLOWED_HOSTS`` that is an empty list. // Assert that a ``find`` operation fails with a client-side error. // Close the client. context('when ALLOWED_HOSTS is empty', function () { before(function () { + // Create a default OIDC client, with an ``ALLOWED_HOSTS`` that is an empty list. client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { ALLOWED_HOSTS: [], - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) + OIDC_TOKEN_CALLBACK: createRequestCallback() } }); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); + // Assert that a ``find`` operation fails with a client-side error. + // Close the client. it('fails validation', async function () { const error = await collection.findOne().catch(error => error); expect(error).to.be.instanceOf(MongoInvalidArgumentError); @@ -255,7 +208,7 @@ describe('MONGODB-OIDC', function () { // { // authMechanismProperties: { // ALLOWED_HOSTS: ['example.com'], - // REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) + // OIDC_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) // } // } // ); @@ -281,10 +234,10 @@ describe('MONGODB-OIDC', function () { client = new MongoClient('mongodb://evilmongodb.com/?authMechanism=MONGODB-OIDC', { authMechanismProperties: { ALLOWED_HOSTS: ['*mongodb.com'], - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) + OIDC_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) } }); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); it('fails validation', async function () { @@ -296,73 +249,6 @@ describe('MONGODB-OIDC', function () { }); }); }); - - describe('1.7 Lock Avoids Extra Callback Calls', function () { - let requestCounter = 0; - - before(function () { - cache.clear(); - }); - - const requestCallback = async () => { - requestCounter++; - if (requestCounter > 1) { - throw new Error('Request callback was entered simultaneously.'); - } - const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { - encoding: 'utf8' - }); - await sleep(3000); - requestCounter--; - return generateResult(token, 300); - }; - const refreshCallback = createRefreshCallback(); - const requestSpy = sinon.spy(requestCallback); - const refreshSpy = sinon.spy(refreshCallback); - - const createClient = () => { - return new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy - } - }); - }; - - const authenticate = async () => { - const client = createClient(); - await client.db('test').collection('test').findOne(); - await client.close(); - }; - - const testPromise = async () => { - await authenticate(); - await authenticate(); - }; - - // Clear the cache. - // Create a request callback that returns a token that will expire soon, and - // a refresh callback. Ensure that the request callback has a time delay, and - // that we can record the number of times each callback is called. - // Spawn two threads that do the following: - // - Create a client with the callbacks. - // - Run a find operation that succeeds. - // - Close the client. - // - Create a new client with the callbacks. - // - Run a find operation that succeeds. - // - Close the client. - // Join the two threads. - // Ensure that the request callback has been called once, and the refresh - // callback has been called twice. - it('does not simultaneously enter a callback', async function () { - await Promise.all([testPromise(), testPromise()]); - // The request callback will get called twice, but will not be entered - // simultaneously. If it does, the function will throw and we'll have - // and exception here. - expect(requestSpy).to.have.been.calledTwice; - expect(refreshSpy).to.have.been.calledTwice; - }); - }); }); describe('2. AWS Automatic Auth', function () { @@ -378,7 +264,7 @@ describe('MONGODB-OIDC', function () { client = new MongoClient( 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' ); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws. @@ -395,7 +281,7 @@ describe('MONGODB-OIDC', function () { client = new MongoClient( 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' ); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. @@ -419,7 +305,7 @@ describe('MONGODB-OIDC', function () { client = new MongoClient( 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' ); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); after(function () { @@ -447,7 +333,7 @@ describe('MONGODB-OIDC', function () { } } ); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws, and an ALLOWED_HOSTS that is an empty list. @@ -469,55 +355,42 @@ describe('MONGODB-OIDC', function () { }); describe('3.1 Valid Callbacks', function () { + // Create request callback that validates its inputs and returns a valid token. const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); - const refreshSpy = sinon.spy(createRefreshCallback()); const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy + OIDC_TOKEN_CALLBACK: requestSpy }; before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create a client that uses the above callbacks. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: authMechanismProperties }); - collection = client.db('test').collection('test'); - await collection.findOne(); - expect(requestSpy).to.have.been.calledOnce; - await client.close(); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create request and refresh callback that validate their inputs and return a valid token. The request callback must return a token that expires in one minute. - // Create a client that uses the above callbacks. - // Perform a find operation that succeeds. Verify that the request callback was called with the appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. - // Perform another find operation that succeeds. Verify that the refresh callback was called with the appropriate inputs, including the timeout parameter if possible. + // Perform a find operation that succeeds. Verify that the request callback was called with the + // appropriate inputs, including the timeout parameter if possible. Ensure that there are no unexpected fields. // Close the client. it('successfully authenticates with the request and refresh callbacks', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - collection = client.db('test').collection('test'); await collection.findOne(); - expect(refreshSpy).to.have.been.calledOnce; + expect(requestSpy).to.have.been.calledOnce; }); }); describe('3.2 Request Callback Returns Null', function () { before(function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create a client with a request callback that returns null. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: () => { + OIDC_TOKEN_CALLBACK: () => { return Promise.resolve(null); } } }); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a client with a request callback that returns null. // Perform a find operation that fails. // Close the client. it('fails authentication', async function () { @@ -533,61 +406,24 @@ describe('MONGODB-OIDC', function () { }); }); - describe('3.3 Refresh Callback Returns Null', function () { - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve(null); - } - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - collection = client.db('test').collection('test'); - await collection.findOne(); - await client.close(); - }); - - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns null. - // Perform a find operation that succeeds. - // Perform a find operation that fails. - // Close the client. - it('fails authentication on refresh', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - try { - await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with invlid return from refresh callback'); - } catch (e) { - expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include( - 'User provided OIDC callbacks must return a valid object with an accessToken' - ); - } - }); - }); - describe('3.4 Request Callback Returns Invalid Data', function () { context('when the request callback has missing fields', function () { before(function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: () => { - return Promise.resolve({}); + // Create a client with a request callback that returns data not conforming to + // the OIDCRequestTokenResult with missing field(s). + client = new MongoClient( + `${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, + { + authMechanismProperties: { + OIDC_TOKEN_CALLBACK: () => { + return Promise.resolve({}); + } } } - }); - collection = client.db('test').collection('test'); + ); + collection = client.db('test').collection('nodeOidcTest'); }); - // Clear the cache. - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). // Perform a find operation that fails. // Close the client. it('fails authentication', async function () { @@ -602,291 +438,14 @@ describe('MONGODB-OIDC', function () { } }); }); - - context('when the request callback has extra fields', function () { - before(function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60, { foo: 'bar' }) - } - }); - collection = client.db('test').collection('test'); - }); - - // Create a client with a request callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). - // Perform a find operation that fails. - // Close the client. - it('fails authentication', async function () { - try { - await collection.findOne(); - expect.fail('Expected OIDC auth to fail with extra fields from request callback'); - } catch (e) { - expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include( - 'User provided OIDC callbacks must return a valid object with an accessToken' - ); - } - }); - }); - }); - - describe('3.5 Refresh Callback Returns Missing Data', function () { - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - await client.close(); - }); - - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with missing field(s). - // Create a client with the callbacks. - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same callbacks. - // Perform a find operation that fails. - // Close the client. - it('fails authentication on the refresh', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - try { - await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with missing data from refresh callback'); - } catch (e) { - expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include( - 'User provided OIDC callbacks must return a valid object with an accessToken' - ); - } - }); - }); - - describe('3.6 Refresh Callback Returns Extra Data', function () { - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 60), - REFRESH_TOKEN_CALLBACK: createRefreshCallback('test_user1', 60, { foo: 'bar' }) - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - await client.close(); - }); - - // Clear the cache. - // Create request callback that returns a valid token that will expire in a minute, and a refresh callback that returns data not conforming to the OIDCRequestTokenResult with extra field(s). - // Create a client with the callbacks. - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same callbacks. - // Perform a find operation that fails. - // Close the client. - it('fails authentication on the refresh', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - try { - await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with extra fields from refresh callback'); - } catch (e) { - expect(e).to.be.instanceOf(MongoMissingCredentialsError); - expect(e.message).to.include( - 'User provided OIDC callbacks must return a valid object with an accessToken' - ); - } - }); }); }); - describe('4. Cached Credentials', function () { - let client: MongoClient; - let collection: Collection; - - afterEach(async function () { - await client?.close(); - }); - - describe('4.1 Cache with refresh', function () { - const requestCallback = createRequestCallback('test_user1', 60); - const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 60)); - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshSpy - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - await client.close(); - }); - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in on minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the same request callback and a refresh callback. - // Ensure that a find operation results in a call to the refresh callback. - // Close the client. - it('successfully authenticates and calls the refresh callback', async function () { - // Ensure credentials added to the cache. - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.have.been.calledOnce; - }); - }); - - describe('4.2 Cache with no refresh', function () { - const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy - } - }); - await client.db('test').collection('test').findOne(); - await client.close(); - }); - - // Clear the cache. - // Create a new client with a request callback that gives credentials that expire in one minute. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with the a request callback but no refresh callback. - // Ensure that a find operation results in a call to the request callback. - // Close the client. - it('successfully authenticates and calls only the request callback', async function () { - expect(cache.entries.size).to.equal(1); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy - } - }); - await client.db('test').collection('test').findOne(); - expect(requestSpy).to.have.been.calledTwice; - }); - }); - - describe('4.3 Cache key includes callback', function () { - const firstRequestCallback = createRequestCallback('test_user1'); - const secondRequestCallback = createRequestCallback('test_user1'); - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: firstRequestCallback - } - }); - await client.db('test').collection('test').findOne(); - await client.close(); - }); - - // Clear the cache. - // Create a new client with a request callback that does not give an `expiresInSeconds` value. - // Ensure that a find operation adds credentials to the cache. - // Close the client. - // Create a new client with a different request callback. - // Ensure that a find operation replaces the one-time entry with a new entry to the cache. - // Close the client. - it('replaces expired entries in the cache', async function () { - expect(cache.entries.size).to.equal(1); - const initialKey = cache.entries.keys().next().value; - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: secondRequestCallback - } - }); - await client.db('test').collection('test').findOne(); - expect(cache.entries.size).to.equal(1); - const newKey = cache.entries.keys().next().value; - expect(newKey).to.not.equal(initialKey); - }); - }); - - describe('4.4 Error clears cache', function () { - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 300), - REFRESH_TOKEN_CALLBACK: () => { - return Promise.resolve({}); - } - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - expect(cache.entries.size).to.equal(1); - await client.close(); - }); - - // Clear the cache. - // Create a new client with a valid request callback that gives credentials that expire within 5 minutes and a refresh callback that gives invalid credentials. - // Ensure that a find operation adds a new entry to the cache. - // Ensure that a subsequent find operation results in an error. - // Ensure that the cached token has been cleared. - // Close the client. - it('clears the cache on authentication error', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - try { - await client.db('test').collection('test').findOne(); - expect.fail('Expected OIDC auth to fail with invalid fields from refresh callback'); - } catch (error) { - expect(error).to.be.instanceOf(MongoMissingCredentialsError); - expect(error.message).to.include(''); - expect(cache.entries.size).to.equal(0); - } - }); - }); - - describe('4.5 AWS Automatic workflow does not use cache', function () { - before(function () { - cache.clear(); - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' - ); - collection = client.db('test').collection('test'); - }); - - // Clear the cache. - // Create a new client that uses the AWS automatic workflow. - // Ensure that a find operation does not add credentials to the cache. - // Close the client. - it('authenticates with no cache usage', async function () { - await collection.findOne(); - expect(cache.entries.size).to.equal(0); - }); - }); - }); - - describe('5. Speculative Authentication', function () { + describe('4. Speculative Authentication', function () { let client: MongoClient; const requestCallback = createRequestCallback('test_user1', 600); const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback + OIDC_TOKEN_CALLBACK: requestCallback }; // Removes the fail point. @@ -920,53 +479,41 @@ describe('MONGODB-OIDC', function () { }); before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create a client with a request callback that returns a valid token. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: authMechanismProperties }); + // Set a fail point for saslStart commands of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 2 + // }, + // "data": { + // "failCommands": [ + // "saslStart" + // ], + // "errorCode": 18 + // } + // } + // + // Note + // + // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. + // await setupFailPoint(); - await client.db('test').collection('test').findOne(); - await client.close(); }); - // Clear the cache. - // Create a client with a request callback that returns a valid token that will not expire soon. - // Set a fail point for saslStart commands of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "saslStart" - // ], - // "errorCode": 18 - // } - // } - // - // Note - // - // The driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. - // - // Perform a find operation that succeeds. - // Close the client. - // Create a new client with the same properties without clearing the cache. - // Set a fail point for saslStart commands. // Perform a find operation that succeeds. // Close the client. it('successfully speculative authenticates', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await setupFailPoint(); - const result = await client.db('test').collection('test').findOne(); + const result = await client.db('test').collection('nodeOidcTest').findOne(); expect(result).to.be.null; }); }); - describe('6. Reauthentication', function () { + describe('5. Reauthentication', function () { let client: MongoClient; // Removes the fail point. @@ -977,12 +524,10 @@ describe('MONGODB-OIDC', function () { }); }; - describe('6.1 Succeeds', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 600)); + describe('5.1 Succeeds', function () { + const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshSpy + OIDC_TOKEN_CALLBACK: requestSpy }; const commandStartedEvents: CommandStartedEvent[] = []; const commandSucceededEvents: CommandSucceededEvent[] = []; @@ -1028,13 +573,18 @@ describe('MONGODB-OIDC', function () { }; before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties + // Create a default OIDC client and an event listener. The following assumes that the driver does not + // emit saslStart or saslContinue events. If the driver does emit those events, + // ignore/filter them for the purposes of this test. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { + authMechanismProperties: authMechanismProperties, + monitorCommands: true }); - await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.not.be.called; - client.close(); + // Perform a find operation that succeeds. + // Assert that the request callback has been called once. + // Clear the listener state if possible. + await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledOnce; }); afterEach(async function () { @@ -1042,12 +592,6 @@ describe('MONGODB-OIDC', function () { await client.close(); }); - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Create a client with the callbacks and an event listener. The following assumes that the driver does not emit saslStart or saslContinue events. If the driver does emit those events, ignore/filter them for the purposes of this test. - // Perform a find operation that succeeds. - // Assert that the refresh callback has not been called. - // Clear the listener state if possible. // Force a reauthenication using a failCommand of the form: // // { @@ -1068,20 +612,16 @@ describe('MONGODB-OIDC', function () { // the driver MUST either use a unique appName or explicitly remove the failCommand after the test to prevent leakage. // // Perform another find operation that succeeds. - // Assert that the refresh callback has been called once, if possible. + // Assert that the request callback has been called twice. // Assert that the ordering of list started events is [find], , find. Note that if the listener stat could not be cleared then there will and be extra find command. // Assert that the list of command succeeded events is [find]. // Assert that a find operation failed once during the command execution. // Close the client. it('successfully reauthenticates', async function () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties, - monitorCommands: true - }); - addListeners(); await setupFailPoint(); - await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.have.been.calledOnce; + addListeners(); + await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledTwice; expect(commandStartedEvents.map(event => event.commandName)).to.deep.equal([ 'find', 'find' @@ -1091,12 +631,11 @@ describe('MONGODB-OIDC', function () { }); }); - describe('6.2 Retries and Succeeds with Cache', function () { + describe('5.2 Succeeds no refresh', function () { const requestCallback = createRequestCallback('test_user1', 600); - const refreshCallback = createRefreshCallback('test_user1', 600); + const requestSpy = sinon.spy(requestCallback); const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback + OIDC_TOKEN_CALLBACK: requestSpy }; // Sets up the fail point for the find to reauthenticate. const setupFailPoint = async () => { @@ -1109,18 +648,21 @@ describe('MONGODB-OIDC', function () { times: 1 }, data: { - failCommands: ['find', 'saslStart'], + failCommands: ['find'], errorCode: 391 } }); }; before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create a default OIDC client with a request callback that does not return a refresh token. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: authMechanismProperties }); - await client.db('test').collection('test').findOne(); + // Perform a ``find`` operation that succeeds. + // Assert that the request callback has been called once. + await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledOnce; await setupFailPoint(); }); @@ -1129,9 +671,71 @@ describe('MONGODB-OIDC', function () { await client.close(); }); - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. + // Force a reauthenication using a failCommand of the form: + // + // { + // "configureFailPoint": "failCommand", + // "mode": { + // "times": 1 + // }, + // "data": { + // "failCommands": [ + // "find" + // ], + // "errorCode": 391 + // } + // } + // // Perform a find operation that succeeds. + // Assert that the request callback has been called twice. + // Close the client. + it('successfully authenticates', async function () { + const result = await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledTwice; + expect(result).to.be.null; + }); + }); + + describe('5.3 Succeeds after refresh fails', function () { + const requestCallback = createRequestCallback('test_user1', 600); + const requestSpy = sinon.spy(requestCallback); + const authMechanismProperties = { + OIDC_TOKEN_CALLBACK: requestSpy + }; + // Sets up the fail point for the find to reauthenticate. + const setupFailPoint = async () => { + return await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 2 + }, + data: { + failCommands: ['find', 'saslContinue'], + errorCode: 391 + } + }); + }; + + before(async function () { + // Create a default OIDC client. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { + authMechanismProperties: authMechanismProperties + }); + // Perform a ``find`` operation that succeeds. + // Assert that the request callback has been called once. + await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledOnce; + await setupFailPoint(); + }); + + afterEach(async function () { + await removeFailPoint(); + await client.close(); + }); + // Force a reauthenication using a failCommand of the form: // // { @@ -1141,26 +745,27 @@ describe('MONGODB-OIDC', function () { // }, // "data": { // "failCommands": [ - // "find", "saslStart" + // "find", "saslContinue" // ], // "errorCode": 391 // } // } // // Perform a find operation that succeeds. + // Assert that the request callback has been called three times. // Close the client. it('successfully authenticates', async function () { - const result = await client.db('test').collection('test').findOne(); + const result = await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledThrice; expect(result).to.be.null; }); }); - describe('6.3 Retries and Fails with no Cache', function () { + describe('5.3 Fails', function () { const requestCallback = createRequestCallback('test_user1', 600); - const refreshCallback = createRefreshCallback('test_user1', 600); + const requestSpy = sinon.spy(requestCallback); const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback + OIDC_TOKEN_CALLBACK: requestSpy }; // Sets up the fail point for the find to reauthenticate. const setupFailPoint = async () => { @@ -1180,12 +785,14 @@ describe('MONGODB-OIDC', function () { }; before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + // Create a default OIDC client. + client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { authMechanismProperties: authMechanismProperties }); - await client.db('test').collection('test').findOne(); - cache.clear(); + // Perform a find operation that succeeds (to force a speculative auth). + // Assert that the request callback has been called once. + await client.db('test').collection('nodeOidcTest').findOne(); + expect(requestSpy).to.have.been.calledOnce; await setupFailPoint(); }); @@ -1194,10 +801,6 @@ describe('MONGODB-OIDC', function () { await client.close(); }); - // Clear the cache. - // Create request and refresh callbacks that return valid credentials that will not expire soon. - // Perform a find operation that succeeds (to force a speculative auth). - // Clear the cache. // Force a reauthenication using a failCommand of the form: // // { @@ -1214,17 +817,20 @@ describe('MONGODB-OIDC', function () { // } // // Perform a find operation that fails. + // Assert that the request callback has been called twice. // Close the client. it('fails authentication', async function () { try { - await client.db('test').collection('test').findOne(); + await client.db('test').collection('nodeOidcTest').findOne(); expect.fail('Reauthentication must fail on the saslStart error'); } catch (error) { // This is the saslStart failCommand bubbled up. expect(error).to.be.instanceOf(MongoServerError); + expect(requestSpy).to.have.been.calledTwice; } }); }); }); + // describe('6. Separate Connections Avoid Extra Callback Calls', function () {}); }); }); diff --git a/test/mongodb.ts b/test/mongodb.ts index 18986610e56..2c157bc5168 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -106,13 +106,11 @@ export * from '../src/cmap/auth/mongo_credentials'; export * from '../src/cmap/auth/mongocr'; export * from '../src/cmap/auth/mongodb_aws'; export * from '../src/cmap/auth/mongodb_oidc'; -export * from '../src/cmap/auth/mongodb_oidc/aws_service_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/azure_service_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/azure_token_cache'; -export * from '../src/cmap/auth/mongodb_oidc/callback_lock_cache'; +export * from '../src/cmap/auth/mongodb_oidc/aws_machine_workflow'; +export * from '../src/cmap/auth/mongodb_oidc/azure_machine_workflow'; export * from '../src/cmap/auth/mongodb_oidc/callback_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/service_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/token_entry_cache'; +export * from '../src/cmap/auth/mongodb_oidc/gcp_machine_workflow'; +export * from '../src/cmap/auth/mongodb_oidc/machine_workflow'; export * from '../src/cmap/auth/plain'; export * from '../src/cmap/auth/providers'; export * from '../src/cmap/auth/scram'; diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index fcb2dbf57d3..982edb8b36a 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -480,70 +480,9 @@ "AWS_SESSION_TOKEN": "token!@#$%^&*()_+" } } - }, - { - "description": "should recognise the mechanism and request callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } }, { - "description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest", "oidcRefresh"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true, - "REFRESH_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism and username with request callback (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": "principalName", - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism with aws device (MONGODB-OIDC)", + "description": "should recognise the mechanism with aws provider (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", "valid": true, "credential": { @@ -552,12 +491,12 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "PROVIDER_NAME": "aws" + "PROVIDER_NAME": "aws" } } }, { - "description": "should recognise the mechanism when auth source is explicitly specified and with aws device (MONGODB-OIDC)", + "description": "should recognise the mechanism when auth source is explicitly specified and with provider (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws", "valid": true, "credential": { @@ -566,39 +505,31 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "PROVIDER_NAME": "aws" + "PROVIDER_NAME": "aws" } } }, { - "description": "should throw an exception if username and password are specified (MONGODB-OIDC)", - "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest"], - "valid": false, - "credential": null - }, - { - "description": "should throw an exception if username and deviceName are specified (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:gcp", + "description": "should throw an exception if supplied a password (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", "valid": false, "credential": null }, { - "description": "should throw an exception if specified deviceName is not supported (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:unexisted", + "description": "should throw an exception if username is specified for aws (MONGODB-OIDC)", + "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:aws", "valid": false, "credential": null }, { - "description": "should throw an exception if neither deviceName nor callbacks specified (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", + "description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:invalid", "valid": false, "credential": null }, { - "description": "should throw an exception when only refresh callback is specified (MONGODB-OIDC)", + "description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRefresh"], "valid": false, "credential": null }, diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index 9f8aab4a725..d2658e0309b 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -350,58 +350,7 @@ tests: mechanism: MONGODB-AWS mechanism_properties: AWS_SESSION_TOKEN: token!@#$%^&*()_+ -- description: should recognise the mechanism and request callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism when auth source is explicitly specified - and with request callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external - callback: - - oidcRequest - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism with request and refresh callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - - oidcRefresh - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true - REFRESH_TOKEN_CALLBACK: true -- description: should recognise the mechanism and username with request callback (MONGODB-OIDC) - uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - valid: true - credential: - username: principalName - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism with aws device (MONGODB-OIDC) +- description: should recognise the mechanism with aws provider (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws valid: true credential: @@ -411,8 +360,7 @@ tests: mechanism: MONGODB-OIDC mechanism_properties: PROVIDER_NAME: aws -- description: should recognise the mechanism when auth source is explicitly specified - and with aws device (MONGODB-OIDC) +- description: should recognise the mechanism when auth source is explicitly specified and with provider (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws valid: true credential: @@ -422,35 +370,23 @@ tests: mechanism: MONGODB-OIDC mechanism_properties: PROVIDER_NAME: aws -- description: should throw an exception if username and password are specified (MONGODB-OIDC) - uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - valid: false - credential: -- description: should throw an exception if username and deviceName are specified - (MONGODB-OIDC) - uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:gcp +- description: should throw an exception if supplied a password (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws valid: false credential: -- description: should throw an exception if specified deviceName is not supported - (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:unexisted +- description: should throw an exception if username is specified for aws (MONGODB-OIDC) + uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&PROVIDER_NAME:aws valid: false credential: -- description: should throw an exception if neither deviceName nor callbacks specified - (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC +- description: should throw an exception if specified provider is not supported (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:invalid valid: false credential: -- description: should throw an exception when only refresh callback is specified (MONGODB-OIDC) +- description: should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRefresh valid: false credential: -- description: should throw an exception when unsupported auth property is specified - (MONGODB-OIDC) +- description: should throw an exception when unsupported auth property is specified (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted valid: false credential: diff --git a/test/spec/auth/unified/reauthenticate_with_retry.json b/test/spec/auth/unified/oidc-auth-with-retry.json similarity index 72% rename from test/spec/auth/unified/reauthenticate_with_retry.json rename to test/spec/auth/unified/oidc-auth-with-retry.json index ef110562ede..aeae3288c98 100644 --- a/test/spec/auth/unified/reauthenticate_with_retry.json +++ b/test/spec/auth/unified/oidc-auth-with-retry.json @@ -1,10 +1,11 @@ { - "description": "reauthenticate_with_retry", - "schemaVersion": "1.12", + "description": "OIDC authentication with retry", + "schemaVersion": "1.18", "runOnRequirements": [ { - "minServerVersion": "6.3", - "auth": true + "minServerVersion": "7.0", + "auth": true, + "authMechanism": "MONGODB-OIDC" } ], "createEntities": [ @@ -12,6 +13,10 @@ "client": { "id": "client0", "uriOptions": { + "authMechanism": "MONGODB-OIDC", + "authMechanismProperties": { + "$$placeholder": 1 + }, "retryReads": true, "retryWrites": true }, @@ -26,7 +31,7 @@ "database": { "id": "database0", "client": "client0", - "databaseName": "db" + "databaseName": "test" } }, { @@ -40,40 +45,26 @@ "initialData": [ { "collectionName": "collName", - "databaseName": "db", - "documents": [] + "databaseName": "test", + "documents": [ + + ] } ], "tests": [ { - "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=true", + "description": "A simple find operation should succeed", "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "find" - ], - "errorCode": 391 - } - } - } - }, { "name": "find", "arguments": { - "filter": {} + "filter": { + } }, "object": "collection0", - "expectResult": [] + "expectResult": [ + + ] } ], "expectEvents": [ @@ -84,20 +75,8 @@ "commandStartedEvent": { "command": { "find": "collName", - "filter": {} - } - } - }, - { - "commandFailedEvent": { - "commandName": "find" - } - }, - { - "commandStartedEvent": { - "command": { - "find": "collName", - "filter": {} + "filter": { + } } } }, diff --git a/test/spec/auth/unified/reauthenticate_with_retry.yml b/test/spec/auth/unified/oidc-auth-with-retry.yml similarity index 71% rename from test/spec/auth/unified/reauthenticate_with_retry.yml rename to test/spec/auth/unified/oidc-auth-with-retry.yml index bf7cb56f3c8..47481d963d8 100644 --- a/test/spec/auth/unified/reauthenticate_with_retry.yml +++ b/test/spec/auth/unified/oidc-auth-with-retry.yml @@ -1,13 +1,20 @@ --- -description: reauthenticate_with_retry -schemaVersion: '1.12' +description: "OIDC authentication with retry" +schemaVersion: "1.18" runOnRequirements: -- minServerVersion: '6.3' +- minServerVersion: "7.0" auth: true + authMechanism: "MONGODB-OIDC" createEntities: - client: id: client0 uriOptions: + authMechanism: "MONGODB-OIDC" + # The $$placeholder document should be replaced by auth mechanism + # properties that enable OIDC auth on the target cloud platform. For + # example, when running the test on AWS, replace the $$placeholder + # document with {"PROVIDER_NAME": "aws"}. + authMechanismProperties: { $$placeholder: 1 } retryReads: true retryWrites: true observeEvents: @@ -17,31 +24,18 @@ createEntities: - database: id: database0 client: client0 - databaseName: db + databaseName: test - collection: id: collection0 database: database0 collectionName: collName initialData: - collectionName: collName - databaseName: db + databaseName: test documents: [] tests: -- description: Read command should reauthenticate when receive ReauthenticationRequired - error code and retryReads=true +- description: A simple find operation should succeed operations: - - name: failPoint - object: testRunner - arguments: - client: client0 - failPoint: - configureFailPoint: failCommand - mode: - times: 1 - data: - failCommands: - - find - errorCode: 391 - name: find arguments: filter: {} @@ -50,12 +44,6 @@ tests: expectEvents: - client: client0 events: - - commandStartedEvent: - command: - find: collName - filter: {} - - commandFailedEvent: - commandName: find - commandStartedEvent: command: find: collName diff --git a/test/spec/auth/unified/reauthenticate_without_retry.json b/test/spec/auth/unified/oidc-auth-without-retry.json similarity index 69% rename from test/spec/auth/unified/reauthenticate_without_retry.json rename to test/spec/auth/unified/oidc-auth-without-retry.json index 6fded476344..ad8c93c03ff 100644 --- a/test/spec/auth/unified/reauthenticate_without_retry.json +++ b/test/spec/auth/unified/oidc-auth-without-retry.json @@ -1,19 +1,29 @@ { - "description": "reauthenticate_without_retry", - "schemaVersion": "1.12", + "description": "OIDC authentication without retry", + "schemaVersion": "1.18", "runOnRequirements": [ { - "minServerVersion": "6.3", - "auth": true + "minServerVersion": "7.0", + "auth": true, + "authMechanism": "MONGODB-OIDC" } ], "createEntities": [ + { + "client": { + "id": "authClient" + } + }, { "client": { "id": "client0", "uriOptions": { - "retryReads": false, - "retryWrites": false + "authMechanism": "MONGODB-OIDC", + "authMechanismProperties": { + "$$placeholder": 1 + }, + "retryReads": true, + "retryWrites": true }, "observeEvents": [ "commandStartedEvent", @@ -26,7 +36,7 @@ "database": { "id": "database0", "client": "client0", - "databaseName": "db" + "databaseName": "test" } }, { @@ -40,40 +50,26 @@ "initialData": [ { "collectionName": "collName", - "databaseName": "db", - "documents": [] + "databaseName": "test", + "documents": [ + + ] } ], "tests": [ { - "description": "Read command should reauthenticate when receive ReauthenticationRequired error code and retryReads=false", + "description": "A simple find operation should succeed", "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "find" - ], - "errorCode": 391 - } - } - } - }, { "name": "find", "arguments": { - "filter": {} + "filter": { + } }, "object": "collection0", - "expectResult": [] + "expectResult": [ + + ] } ], "expectEvents": [ @@ -84,20 +80,8 @@ "commandStartedEvent": { "command": { "find": "collName", - "filter": {} - } - } - }, - { - "commandFailedEvent": { - "commandName": "find" - } - }, - { - "commandStartedEvent": { - "command": { - "find": "collName", - "filter": {} + "filter": { + } } } }, @@ -111,7 +95,7 @@ ] }, { - "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=false", + "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true", "operations": [ { "name": "failPoint", diff --git a/test/spec/auth/unified/reauthenticate_without_retry.yml b/test/spec/auth/unified/oidc-auth-without-retry.yml similarity index 68% rename from test/spec/auth/unified/reauthenticate_without_retry.yml rename to test/spec/auth/unified/oidc-auth-without-retry.yml index 394c4be91e0..16b4978d1d1 100644 --- a/test/spec/auth/unified/reauthenticate_without_retry.yml +++ b/test/spec/auth/unified/oidc-auth-without-retry.yml @@ -1,15 +1,24 @@ --- -description: reauthenticate_without_retry -schemaVersion: '1.13' +description: "OIDC authentication without retry" +schemaVersion: "1.18" runOnRequirements: -- minServerVersion: '6.3' +- minServerVersion: "7.0" auth: true + authMechanism: "MONGODB-OIDC" createEntities: +- client: + id: authClient - client: id: client0 uriOptions: - retryReads: false - retryWrites: false + authMechanism: "MONGODB-OIDC" + # The $$placeholder document should be replaced by auth mechanism + # properties that enable OIDC auth on the target cloud platform. For + # example, when running the test on AWS, replace the $$placeholder + # document with {"PROVIDER_NAME": "aws"}. + authMechanismProperties: { $$placeholder: 1 } + retryReads: true + retryWrites: true observeEvents: - commandStartedEvent - commandSucceededEvent @@ -17,31 +26,18 @@ createEntities: - database: id: database0 client: client0 - databaseName: db + databaseName: test - collection: id: collection0 database: database0 collectionName: collName initialData: - collectionName: collName - databaseName: db + databaseName: test documents: [] tests: -- description: Read command should reauthenticate when receive ReauthenticationRequired - error code and retryReads=false +- description: A simple find operation should succeed operations: - - name: failPoint - object: testRunner - arguments: - client: client0 - failPoint: - configureFailPoint: failCommand - mode: - times: 1 - data: - failCommands: - - find - errorCode: 391 - name: find arguments: filter: {} @@ -50,12 +46,6 @@ tests: expectEvents: - client: client0 events: - - commandStartedEvent: - command: - find: collName - filter: {} - - commandFailedEvent: - commandName: find - commandStartedEvent: command: find: collName @@ -63,7 +53,7 @@ tests: - commandSucceededEvent: commandName: find - description: Write command should reauthenticate when receive ReauthenticationRequired - error code and retryWrites=false + error code and retryWrites=true operations: - name: failPoint object: testRunner diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index e4f3dd52a3f..b4261bc414f 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -158,6 +158,10 @@ export class TestConfiguration { return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf('PROVIDER_NAME:azure') > -1; } + isGcpOIDC(uri: string): boolean { + return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf('PROVIDER_NAME:gcp') > -1; + } + newClient(urlOrQueryOptions?: string | Record, serverOptions?: Record) { serverOptions = Object.assign({}, getEnvironmentalOptions(), serverOptions); @@ -340,6 +344,11 @@ export class TestConfiguration { url.searchParams.append('authSource', 'admin'); } + if (this.uri.includes('MONGODB-OIDC')) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return process.env.MONGODB_URI!; + } + const connectionString = url.toString().replace(FILLER_HOST, actualHostsString); return connectionString; diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index e947a6f069d..08089bb8fc6 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -113,13 +113,6 @@ const testConfigBeforeHook = async function () { this.configuration = new AstrolabeTestConfiguration(process.env.DRIVERS_ATLAS_TESTING_URI, {}); return; } - // TODO(NODE-5035): Implement OIDC support. Creating the MongoClient will fail - // with "MongoInvalidArgumentError: AuthMechanism 'MONGODB-OIDC' not supported" - // as is expected until that ticket goes in. Then this condition gets removed. - if (MONGODB_URI && MONGODB_URI.includes('MONGODB-OIDC')) { - this.configuration = new TestConfiguration(MONGODB_URI, {}); - return; - } const client = new MongoClient(loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, { ...getEnvironmentalOptions(), diff --git a/test/tools/unified-spec-runner/runner.ts b/test/tools/unified-spec-runner/runner.ts index 9f1fb3925f5..4a18655619f 100644 --- a/test/tools/unified-spec-runner/runner.ts +++ b/test/tools/unified-spec-runner/runner.ts @@ -73,6 +73,13 @@ async function runUnifiedTest( if (ctx.configuration.isLoadBalanced) { // The util client can always point at the single mongos LB frontend. utilClient = ctx.configuration.newClient(ctx.configuration.singleMongosLoadBalancerUri); + } else if (process.env.UTIL_CLIENT_USER && process.env.UTIL_CLIENT_PASSWORD) { + utilClient = ctx.configuration.newClient({ + auth: { + username: process.env.UTIL_CLIENT_USER, + password: process.env.UTIL_CLIENT_PASSWORD + } + }); } else { utilClient = ctx.configuration.newClient(); } diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index 3b3daef8042..f9bbd534e9f 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -107,6 +107,7 @@ export type TopologyId = (typeof TopologyType)[keyof typeof TopologyType]; export interface RunOnRequirement { serverless?: 'forbid' | 'allow' | 'require'; auth?: boolean; + authMechanism?: string; maxServerVersion?: string; minServerVersion?: string; topologies?: TopologyId[]; diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index 233274b2925..374ae467927 100644 --- a/test/tools/unified-spec-runner/unified-utils.ts +++ b/test/tools/unified-spec-runner/unified-utils.ts @@ -100,6 +100,13 @@ export async function topologySatisfies( if (!ok && skipReason == null) { skipReason = `requires auth but auth is not enabled`; } + if ( + r.authMechanism && + !config.parameters.authenticationMechanisms.includes(r.authMechanism) + ) { + ok &&= false; + skipReason = `requires ${r.authMechanism} to be supported by the server`; + } } else if (r.auth === false) { ok &&= process.env.AUTH === 'noauth' || process.env.AUTH == null; if (!ok && skipReason == null) skipReason = `requires no auth but auth is enabled`; @@ -203,7 +210,16 @@ export function makeConnectionString( ): string { const connectionString = new ConnectionString(uri); for (const [name, value] of Object.entries(uriOptions ?? {})) { - connectionString.searchParams.set(name, String(value)); + // If name is authMechanismProperties and value is { $$placeholder: 1 } + // Then look at the environment for the proper value to set. + if (name === 'authMechanismProperties' && '$$placeholder' in (value as any)) { + // If we're in AWS set the PROVIDER_NAME. + if (process.env.AWS_WEB_IDENTITY_TOKEN_FILE) { + connectionString.searchParams.set(name, 'PROVIDER_NAME:aws'); + } + } else { + connectionString.searchParams.set(name, String(value)); + } } return connectionString.toString(); } diff --git a/test/tools/uri_spec_runner.ts b/test/tools/uri_spec_runner.ts index 6ad6e863819..09b7d24cdc6 100644 --- a/test/tools/uri_spec_runner.ts +++ b/test/tools/uri_spec_runner.ts @@ -91,15 +91,11 @@ export function executeUriValidationTest( const CALLBACKS = { oidcRequest: async () => { return { accessToken: '' }; - }, - oidcRefresh: async () => { - return { accessToken: '' }; } }; const CALLBACK_MAPPINGS = { - oidcRequest: 'REQUEST_TOKEN_CALLBACK', - oidcRefresh: 'REFRESH_TOKEN_CALLBACK' + oidcRequest: 'OIDC_TOKEN_CALLBACK' }; const mongoClientOptions = {}; @@ -223,10 +219,7 @@ export function executeUriValidationTest( // TODO(NODE-3925): Ensure default SERVICE_NAME is set on the parsed mechanism properties continue; } - if ( - expectedMechProp === 'REQUEST_TOKEN_CALLBACK' || - expectedMechProp === 'REFRESH_TOKEN_CALLBACK' - ) { + if (expectedMechProp === 'OIDC_TOKEN_CALLBACK') { expect( options, `${errorMessage} credentials.mechanismProperties.${expectedMechProp}` diff --git a/test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/aws_machine_workflow.test.ts similarity index 85% rename from test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts rename to test/unit/cmap/auth/mongodb_oidc/aws_machine_workflow.test.ts index 55438240e7f..12d6be155c5 100644 --- a/test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/aws_machine_workflow.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { AwsServiceWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; +import { AwsMachineWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; -describe('AwsDeviceWorkFlow', function () { +describe('AwsMachineFlow', function () { describe('#execute', function () { - const workflow = new AwsServiceWorkflow(); + const workflow = new AwsMachineWorkflow(); context('when AWS_WEB_IDENTITY_TOKEN_FILE is not in the env', function () { let file; diff --git a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts new file mode 100644 index 00000000000..03c79328123 --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { AzureMachineWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; + +describe('AzureMachineFlow', function () { + describe('#execute', function () { + const workflow = new AzureMachineWorkflow(); + + context('when TOKEN_AUDIENCE is not set', function () { + const connection = sinon.createStubInstance(Connection); + const credentials = sinon.createStubInstance(MongoCredentials); + + it('throws an error', async function () { + try { + await workflow.execute(connection, credentials); + expect.fail('workflow must fail without TOKEN_AUDIENCE'); + } catch (error) { + expect(error.message).to.include('TOKEN_AUDIENCE'); + } + }); + }); + }); +}); diff --git a/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts deleted file mode 100644 index ac95eb8a9c3..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { expect } from 'chai'; - -import { AzureTokenCache } from '../../../../mongodb'; - -describe('AzureTokenCache', function () { - const tokenResultWithExpiration = Object.freeze({ - access_token: 'test', - expires_in: 100 - }); - - describe('#addEntry', function () { - context('when expiresInSeconds is provided', function () { - const cache = new AzureTokenCache(); - let entry; - - before(function () { - cache.addEntry('audience', tokenResultWithExpiration); - entry = cache.getEntry('audience'); - }); - - it('adds the token result', function () { - expect(entry.token).to.equal('test'); - }); - - it('creates an expiration', function () { - expect(entry.expiration).to.be.within(Date.now(), Date.now() + 100 * 1000); - }); - }); - }); - - describe('#clear', function () { - const cache = new AzureTokenCache(); - - before(function () { - cache.addEntry('audience', tokenResultWithExpiration); - cache.clear(); - }); - - it('clears the cache', function () { - expect(cache.entries.size).to.equal(0); - }); - }); - - describe('#deleteEntry', function () { - const cache = new AzureTokenCache(); - - before(function () { - cache.addEntry('audience', tokenResultWithExpiration); - cache.deleteEntry('audience'); - }); - - it('deletes the entry', function () { - expect(cache.getEntry('audience')).to.not.exist; - }); - }); - - describe('#getEntry', function () { - const cache = new AzureTokenCache(); - - before(function () { - cache.addEntry('audience1', tokenResultWithExpiration); - cache.addEntry('audience2', tokenResultWithExpiration); - }); - - context('when there is a matching entry', function () { - it('returns the entry', function () { - expect(cache.getEntry('audience1')?.token).to.equal('test'); - }); - }); - - context('when there is no matching entry', function () { - it('returns undefined', function () { - expect(cache.getEntry('audience')).to.equal(undefined); - }); - }); - }); -}); diff --git a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts deleted file mode 100644 index d10490fa5b0..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; - -import { - CallbackLockCache, - Connection, - MongoCredentials, - MongoInvalidArgumentError -} from '../../../../mongodb'; -import { sleep } from '../../../../tools/utils'; - -describe('CallbackLockCache', function () { - describe('#getCallbacks', function () { - const connection = sinon.createStubInstance(Connection); - connection.address = 'localhost:27017'; - - context('when a request callback does not exist', function () { - const credentials = new MongoCredentials({ - username: 'test_user', - password: 'pwd', - source: '$external', - mechanismProperties: {} - }); - const cache = new CallbackLockCache(); - - it('raises an error', function () { - try { - cache.getEntry(connection, credentials); - expect.fail('Must raise error when no request callback exists.'); - } catch (error) { - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include( - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required' - ); - } - }); - }); - - context('when no entry exists in the cache', function () { - context('when a refresh callback exists', function () { - let requestCount = 0; - let refreshCount = 0; - - const request = async () => { - requestCount++; - if (requestCount > 1) { - throw new Error('Cannot execute request simultaneously.'); - } - await sleep(1000); - requestCount--; - return { accessToken: '' }; - }; - const refresh = async () => { - refreshCount++; - if (refreshCount > 1) { - throw new Error('Cannot execute refresh simultaneously.'); - } - await sleep(1000); - refreshCount--; - return Promise.resolve({ accessToken: '' }); - }; - const requestSpy = sinon.spy(request); - const refreshSpy = sinon.spy(refresh); - const credentials = new MongoCredentials({ - username: 'test_user', - password: 'pwd', - source: '$external', - mechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy - } - }); - const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback, callbackHash } = cache.getEntry( - connection, - credentials - ); - - it('puts a new entry in the cache', function () { - expect(cache.entries).to.have.lengthOf(1); - }); - - it('returns the new entry', function () { - expect(requestCallback).to.exist; - expect(refreshCallback).to.exist; - expect(callbackHash).to.exist; - }); - - it('locks the callbacks', async function () { - await Promise.allSettled([ - requestCallback(), - requestCallback(), - refreshCallback(), - refreshCallback() - ]); - expect(requestSpy).to.have.been.calledTwice; - expect(refreshSpy).to.have.been.calledTwice; - }); - }); - - context('when a refresh function does not exist', function () { - let requestCount = 0; - - const request = async () => { - requestCount++; - if (requestCount > 1) { - throw new Error('Cannot execute request simultaneously.'); - } - await sleep(1000); - requestCount--; - return Promise.resolve({ accessToken: '' }); - }; - const requestSpy = sinon.spy(request); - const credentials = new MongoCredentials({ - username: 'test_user', - password: 'pwd', - source: '$external', - mechanismProperties: { - REQUEST_TOKEN_CALLBACK: requestSpy - } - }); - const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback, callbackHash } = cache.getEntry( - connection, - credentials - ); - - it('puts a new entry in the cache', function () { - expect(cache.entries).to.have.lengthOf(1); - }); - - it('returns the new entry', function () { - expect(requestCallback).to.exist; - expect(refreshCallback).to.not.exist; - expect(callbackHash).to.exist; - }); - - it('locks the callbacks', async function () { - await Promise.allSettled([requestCallback(), requestCallback()]); - expect(requestSpy).to.have.been.calledTwice; - }); - }); - }); - }); -}); diff --git a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts new file mode 100644 index 00000000000..2655c6ebbc2 --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -0,0 +1,24 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Connection, GCPMachineWorkflow, MongoCredentials } from '../../../../mongodb'; + +describe('GCPMachineFlow', function () { + describe('#execute', function () { + const workflow = new GCPMachineWorkflow(); + + context('when TOKEN_AUDIENCE is not set', function () { + const connection = sinon.createStubInstance(Connection); + const credentials = sinon.createStubInstance(MongoCredentials); + + it('throws an error', async function () { + try { + await workflow.execute(connection, credentials); + expect.fail('workflow must fail without TOKEN_AUDIENCE'); + } catch (error) { + expect(error.message).to.include('TOKEN_AUDIENCE'); + } + }); + }); + }); +}); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts deleted file mode 100644 index 90f3a940858..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts +++ /dev/null @@ -1,144 +0,0 @@ -import { expect } from 'chai'; - -import { type TokenEntry, TokenEntryCache } from '../../../../mongodb'; - -describe('TokenEntryCache', function () { - const tokenResultWithExpiration = Object.freeze({ - accessToken: 'test', - expiresInSeconds: 100 - }); - const serverResult = Object.freeze({ - issuer: 'test', - clientId: '1' - }); - const callbackHash = '1'; - - describe('#addEntry', function () { - context('when expiresInSeconds is provided', function () { - const cache = new TokenEntryCache(); - let entry; - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); - entry = cache.getEntry('localhost', 'user', callbackHash); - }); - - it('adds the token result', function () { - expect(entry.tokenResult).to.deep.equal(tokenResultWithExpiration); - }); - - it('adds the server result', function () { - expect(entry.serverInfo).to.deep.equal(serverResult); - }); - - it('creates an expiration', function () { - expect(entry.expiration).to.be.within(Date.now(), Date.now() + 100 * 1000); - }); - }); - - context('when expiresInSeconds is not provided', function () { - const cache = new TokenEntryCache(); - let entry: TokenEntry | undefined; - - const expiredResult = Object.freeze({ accessToken: 'test' }); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, expiredResult, serverResult); - entry = cache.getEntry('localhost', 'user', callbackHash); - }); - - it('sets an immediate expiration', function () { - expect(entry?.expiration).to.be.at.most(Date.now()); - }); - }); - - context('when expiresInSeconds is null', function () { - const cache = new TokenEntryCache(); - let entry: TokenEntry | undefined; - - const expiredResult = Object.freeze({ - accessToken: 'test', - expiredInSeconds: null - }); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, expiredResult, serverResult); - entry = cache.getEntry('localhost', 'user', callbackHash); - }); - - it('sets an immediate expiration', function () { - expect(entry?.expiration).to.be.at.most(Date.now()); - }); - }); - }); - - describe('#clear', function () { - const cache = new TokenEntryCache(); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); - cache.clear(); - }); - - it('clears the cache', function () { - expect(cache.entries.size).to.equal(0); - }); - }); - - describe('#deleteExpiredEntries', function () { - const cache = new TokenEntryCache(); - - const nonExpiredResult = Object.freeze({ - accessToken: 'test', - expiresInSeconds: 600 - }); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); - cache.addEntry('localhost', 'user2', callbackHash, nonExpiredResult, serverResult); - cache.deleteExpiredEntries(); - }); - - it('deletes all expired tokens from the cache 5 minutes before expiredInSeconds', function () { - expect(cache.entries.size).to.equal(1); - expect(cache.getEntry('localhost', 'user', callbackHash)).to.not.exist; - expect(cache.getEntry('localhost', 'user2', callbackHash)).to.exist; - }); - }); - - describe('#deleteEntry', function () { - const cache = new TokenEntryCache(); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); - cache.deleteEntry('localhost', 'user', callbackHash); - }); - - it('deletes the entry', function () { - expect(cache.getEntry('localhost', 'user', callbackHash)).to.not.exist; - }); - }); - - describe('#getEntry', function () { - const cache = new TokenEntryCache(); - - before(function () { - cache.addEntry('localhost', 'user', callbackHash, tokenResultWithExpiration, serverResult); - cache.addEntry('localhost', 'user2', callbackHash, tokenResultWithExpiration, serverResult); - }); - - context('when there is a matching entry', function () { - it('returns the entry', function () { - expect(cache.getEntry('localhost', 'user', callbackHash)?.tokenResult).to.equal( - tokenResultWithExpiration - ); - }); - }); - - context('when there is no matching entry', function () { - it('returns undefined', function () { - expect(cache.getEntry('localhost', 'user1', callbackHash)).to.equal(undefined); - }); - }); - }); -});