From e41a69da3bbf80fa73767b9dbca4c4623ba11924 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 Nov 2023 13:43:22 -0500 Subject: [PATCH 01/64] feat(NODE-5464): OIDC machine workflow --- .evergreen/config.in.yml | 179 ++-- .evergreen/config.yml | 208 +++-- .evergreen/generate_evergreen_tasks.js | 41 +- .evergreen/run-oidc-prose-tests.sh | 26 + .evergreen/run-oidc-tests-azure.sh | 5 +- .evergreen/run-oidc-tests-gcp.sh | 10 + .evergreen/run-oidc-tests-test.sh | 11 + .evergreen/run-oidc-tests.sh | 35 - .evergreen/run-oidc-unified-tests.sh | 16 + .evergreen/setup-oidc-roles.sh | 8 - package.json | 3 + src/cmap/auth/mongo_credentials.ts | 60 +- src/cmap/auth/mongodb_oidc.ts | 76 +- .../automated_callback_workflow.ts | 61 ++ .../auth/mongodb_oidc/aws_service_workflow.ts | 29 - .../mongodb_oidc/azure_machine_workflow.ts | 70 ++ .../mongodb_oidc/azure_service_workflow.ts | 86 -- .../auth/mongodb_oidc/azure_token_cache.ts | 51 -- src/cmap/auth/mongodb_oidc/cache.ts | 63 -- .../auth/mongodb_oidc/callback_lock_cache.ts | 115 --- .../auth/mongodb_oidc/callback_workflow.ts | 254 ++---- .../auth/mongodb_oidc/command_builders.ts | 43 + .../auth/mongodb_oidc/gcp_machine_workflow.ts | 41 + .../mongodb_oidc/human_callback_workflow.ts | 61 ++ .../auth/mongodb_oidc/machine_workflow.ts | 83 ++ .../auth/mongodb_oidc/service_workflow.ts | 49 -- src/cmap/auth/mongodb_oidc/token_cache.ts | 32 + .../auth/mongodb_oidc/token_entry_cache.ts | 77 -- .../mongodb_oidc/token_machine_workflow.ts | 30 + src/error.ts | 28 + src/index.ts | 10 +- src/mongo_client.ts | 5 +- src/mongo_client_auth_providers.ts | 3 +- .../auth/mongodb_oidc_azure.prose.test.ts | 213 +---- .../auth/mongodb_oidc_gcp.prose.test.ts | 54 ++ .../auth/mongodb_oidc_test.prose.test.ts | 501 +++++++++++ test/manual/mongodb_oidc.prose.test.ts | 832 +++++------------- test/mongodb.ts | 10 +- test/spec/auth/legacy/connection-string.json | 95 +- test/spec/auth/legacy/connection-string.yml | 92 +- ...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 | 12 +- test/tools/runner/hooks/configuration.js | 9 +- test/tools/unified-spec-runner/entities.ts | 45 +- test/tools/unified-spec-runner/runner.ts | 10 + test/tools/unified-spec-runner/schema.ts | 1 + .../unified-spec-runner/unified-utils.ts | 29 +- test/tools/uri_spec_runner.ts | 11 +- test/unit/cmap/auth/mongodb_oidc.test.ts | 4 +- .../mongodb_oidc/aws_service_workflow.test.ts | 34 - .../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 --- .../token_machine_workflow.test.ts | 34 + test/unit/connection_string.test.ts | 22 +- test/unit/index.test.ts | 2 + 61 files changed, 2033 insertions(+), 2485 deletions(-) create mode 100755 .evergreen/run-oidc-prose-tests.sh create mode 100644 .evergreen/run-oidc-tests-gcp.sh create mode 100644 .evergreen/run-oidc-tests-test.sh delete mode 100755 .evergreen/run-oidc-tests.sh create mode 100755 .evergreen/run-oidc-unified-tests.sh delete mode 100644 .evergreen/setup-oidc-roles.sh create mode 100644 src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts delete mode 100644 src/cmap/auth/mongodb_oidc/aws_service_workflow.ts create mode 100644 src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts delete mode 100644 src/cmap/auth/mongodb_oidc/azure_service_workflow.ts 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/human_callback_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 src/cmap/auth/mongodb_oidc/token_machine_workflow.ts create mode 100644 test/integration/auth/mongodb_oidc_gcp.prose.test.ts create mode 100644 test/integration/auth/mongodb_oidc_test.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%) delete mode 100644 test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts 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 create mode 100644 test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index da9d6158081..f420d1dedef 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -123,58 +123,6 @@ functions: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} - "bootstrap oidc": - - command: ec2.assume_role - params: - role_arn: ${OIDC_AWS_ROLE_ARN} - - command: shell.exec - type: test - params: - working_dir: "src" - shell: bash - script: | - ${PREPARE_SHELL} - cd "${DRIVERS_TOOLS}"/.evergreen/auth_oidc - - # This is a bit confusing but the ec2.assume_role command before - # this task will overwrite these variables to a different value - # than we have set in our evergreen project config. As these are - # now specific to the OIDC ARN, we re-export for the python - # scripts. - export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} - export OIDC_TOKEN_DIR=/tmp/tokens - - . ./activate-authoidcvenv.sh - python oidc_write_orchestration.py - python oidc_get_tokens.py - - "setup oidc roles": - - command: subprocess.exec - params: - working_dir: src - binary: bash - args: - - .evergreen/setup-oidc-roles.sh - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - - "run oidc tests aws": - - command: shell.exec - type: test - params: - working_dir: "src" - timeout_secs: 300 - shell: bash - script: | - ${PREPARE_SHELL} - - OIDC_TOKEN_DIR="/tmp/tokens" \ - AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ - PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ - bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh - "run tests": - command: shell.exec type: test @@ -1260,11 +1208,6 @@ tasks: - name: "oidc-auth-test-azure-latest" commands: - - command: expansions.update - type: setup - params: - updates: - - { key: NPM_VERSION, value: "9" } - func: "install dependencies" - command: subprocess.exec params: @@ -1273,11 +1216,74 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} - PROVIDER_NAME: azure + ENVIRONMENT: azure + SCRIPT: run-oidc-prose-tests.sh + args: + - .evergreen/run-oidc-tests-azure.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: azure + SCRIPT: run-oidc-unified-tests.sh args: - .evergreen/run-oidc-tests-azure.sh + - name: "oidc-auth-test-test-latest" + commands: + - func: "install dependencies" + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: test + SCRIPT: run-oidc-prose-tests.sh + args: + - .evergreen/run-oidc-tests-test.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: aws + SCRIPT: run-oidc-unified-tests.sh + args: + - .evergreen/run-oidc-tests-aws.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} + ENVIRONMENT: gcp + SCRIPT: run-oidc-prose-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: gcp + SCRIPT: run-oidc-unified-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + - name: "test-aws-lambda-deployed" commands: - command: expansions.update @@ -1428,6 +1434,23 @@ task_groups: tasks: - test-azurekms-task + - name: testtestoidc_task_group + setup_group: + - func: fetch source + - command: ec2.assume_role + params: + role_arn: ${OIDC_AWS_ROLE_ARN} + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-test-latest + - name: testazureoidc_task_group setup_group: - func: fetch source @@ -1437,25 +1460,43 @@ 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: + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/setup.sh + teardown_task: - command: shell.exec params: shell: bash script: |- ${PREPARE_SHELL} - $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/teardown.sh setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - oidc-auth-test-azure-latest + - 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/setup.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest + - name: test_atlas_task_group setup_group: - func: fetch source @@ -1471,7 +1512,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -1499,7 +1540,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 7980b5fd326..cf548c55127 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -96,55 +96,6 @@ functions: - .evergreen/run-azure-kms-mock-server.sh env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} - bootstrap oidc: - - command: ec2.assume_role - params: - role_arn: ${OIDC_AWS_ROLE_ARN} - - command: shell.exec - type: test - params: - working_dir: src - shell: bash - script: | - ${PREPARE_SHELL} - cd "${DRIVERS_TOOLS}"/.evergreen/auth_oidc - - # This is a bit confusing but the ec2.assume_role command before - # this task will overwrite these variables to a different value - # than we have set in our evergreen project config. As these are - # now specific to the OIDC ARN, we re-export for the python - # scripts. - export AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - export AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - export AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN} - export OIDC_TOKEN_DIR=/tmp/tokens - - . ./activate-authoidcvenv.sh - python oidc_write_orchestration.py - python oidc_get_tokens.py - setup oidc roles: - - command: subprocess.exec - params: - working_dir: src - binary: bash - args: - - .evergreen/setup-oidc-roles.sh - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - run oidc tests aws: - - command: shell.exec - type: test - params: - working_dir: src - timeout_secs: 300 - shell: bash - script: | - ${PREPARE_SHELL} - - OIDC_TOKEN_DIR="/tmp/tokens" \ - AWS_WEB_IDENTITY_TOKEN_FILE="/tmp/tokens/test_user1" \ - PROJECT_DIRECTORY="${PROJECT_DIRECTORY}" \ - bash ${PROJECT_DIRECTORY}/.evergreen/run-oidc-tests.sh run tests: - command: shell.exec type: test @@ -1211,11 +1162,6 @@ tasks: - src/.evergreen/run-azure-kms-tests.sh - name: oidc-auth-test-azure-latest commands: - - command: expansions.update - type: setup - params: - updates: - - {key: NPM_VERSION, value: '9'} - func: install dependencies - command: subprocess.exec params: @@ -1224,10 +1170,71 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} - PROVIDER_NAME: azure + ENVIRONMENT: azure + SCRIPT: run-oidc-prose-tests.sh args: - .evergreen/run-oidc-tests-azure.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: azure + SCRIPT: run-oidc-unified-tests.sh + args: + - .evergreen/run-oidc-tests-azure.sh + - name: oidc-auth-test-test-latest + commands: + - func: install dependencies + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: test + SCRIPT: run-oidc-prose-tests.sh + args: + - .evergreen/run-oidc-tests-test.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: aws + SCRIPT: run-oidc-unified-tests.sh + args: + - .evergreen/run-oidc-tests-aws.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} + ENVIRONMENT: gcp + SCRIPT: run-oidc-prose-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh + - command: subprocess.exec + params: + working_dir: src + binary: bash + env: + DRIVERS_TOOLS: ${DRIVERS_TOOLS} + PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} + ENVIRONMENT: gcp + SCRIPT: run-oidc-unified-tests.sh + args: + - .evergreen/run-oidc-tests-gcp.sh - name: test-aws-lambda-deployed commands: - command: expansions.update @@ -1946,25 +1953,6 @@ tasks: commands: - func: install dependencies - func: run ldap tests - - name: test-auth-oidc - 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 tests aws - name: test-socks5 tags: [] commands: @@ -4467,6 +4455,25 @@ task_groups: - ${DRIVERS_TOOLS}/.evergreen/csfle/azurekms/teardown.sh tasks: - test-azurekms-task + - name: testtestoidc_task_group + setup_group: + - func: fetch source + - command: ec2.assume_role + params: + role_arn: ${OIDC_AWS_ROLE_ARN} + - command: subprocess.exec + params: + binary: bash + include_expansions_in_env: + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - AWS_SESSION_TOKEN + args: + - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-test-latest - name: testazureoidc_task_group setup_group: - func: fetch source @@ -4476,24 +4483,41 @@ 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: + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/setup.sh + teardown_task: - command: shell.exec params: shell: bash script: |- ${PREPARE_SHELL} - $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh + $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/teardown.sh setup_group_can_fail_task: true setup_group_timeout_secs: 1800 tasks: - oidc-auth-test-azure-latest + - 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/setup.sh + teardown_task: + - command: shell.exec + params: + shell: bash + script: |- + ${PREPARE_SHELL} + $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/teardown.sh + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 + tasks: + - oidc-auth-test-gcp-latest - name: test_atlas_task_group setup_group: - func: fetch source @@ -4509,7 +4533,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -4536,7 +4560,7 @@ task_groups: - command: expansions.update params: file: src/atlas-expansion.yml - teardown_group: + teardown_task: - command: subprocess.exec params: working_dir: src @@ -4615,7 +4639,6 @@ buildvariants: - test-latest-load-balanced - test-auth-kerberos - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4675,7 +4698,6 @@ buildvariants: - test-latest-load-balanced - test-auth-kerberos - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4735,7 +4757,6 @@ buildvariants: - test-latest-load-balanced - test-auth-kerberos - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4794,7 +4815,6 @@ buildvariants: - test-latest-load-balanced - test-auth-kerberos - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -5138,6 +5158,16 @@ buildvariants: tasks: - test_azurekms_task_group - test-azurekms-fail-task + - name: ubuntu20-test-all-oidc + display_name: MONGODB-OIDC Auth Tests + run_on: ubuntu2004-small + expansions: + NODE_LTS_VERSION: 20 + batchtime: 20160 + tasks: + - testtestoidc_task_group + - testazureoidc_task_group + - testgcpoidc_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 406b926cab5..31dfbc84de7 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -165,23 +165,6 @@ TASKS.push( tags: ['auth', 'ldap'], commands: [{ func: 'install dependencies' }, { func: 'run ldap tests' }] }, - { - name: 'test-auth-oidc', - 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 tests aws' } - ] - }, { name: 'test-socks5', tags: [], @@ -705,16 +688,20 @@ 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-all-oidc', + display_name: 'MONGODB-OIDC Auth Tests', + run_on: UBUNTU_20_OS, + expansions: { + NODE_LTS_VERSION: LATEST_LTS + }, + batchtime: 20160, + tasks: [ + 'testtestoidc_task_group', + 'testazureoidc_task_group', + 'testgcpoidc_task_group' + ] +}); BUILD_VARIANTS.push({ name: 'rhel8-test-atlas', diff --git a/.evergreen/run-oidc-prose-tests.sh b/.evergreen/run-oidc-prose-tests.sh new file mode 100755 index 00000000000..51e4bf00afe --- /dev/null +++ b/.evergreen/run-oidc-prose-tests.sh @@ -0,0 +1,26 @@ +#!/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 + +ENVIRONMENT=${ENVIRONMENT:-"aws"} +PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."} +source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" + +printenv + +if [ -z "${MONGODB_URI_SINGLE}" ]; then + echo "Must specify MONGODB_URI_SINGLE" + exit 1 +fi + +if [ "$ENVIRONMENT" = "azure" ]; then + npm run check:oidc-azure +elif [ "$ENVIRONMENT" = "gcp" ]; then + npm run check:oidc-gcp +else + if [ -z "${OIDC_TOKEN_FILE}" ]; then + echo "Must specify OIDC_TOKEN_FILE" + exit 1 + fi + npm run check:oidc-test +fi diff --git a/.evergreen/run-oidc-tests-azure.sh b/.evergreen/run-oidc-tests-azure.sh index 6e65bff3f44..4fa7c5bd55d 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 && ENVIRONMENT=azure ./.evergreen/${SCRIPT}" export PROJECT_DIRECTORY=$PROJECT_DIRECTORY -export PROVIDER_NAME=$PROVIDER_NAME +export ENVIRONMENT=$ENVIRONMENT 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..f2fc1de2dc1 --- /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 && ENVIRONMENT=gcp ./.evergreen/${SCRIPT}" +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +export ENVIRONMENT=$ENVIRONMENT +bash $DRIVERS_TOOLS/.evergreen/auth_oidc/gcp/run-driver-test.sh \ No newline at end of file diff --git a/.evergreen/run-oidc-tests-test.sh b/.evergreen/run-oidc-tests-test.sh new file mode 100644 index 00000000000..59389bc0ca8 --- /dev/null +++ b/.evergreen/run-oidc-tests-test.sh @@ -0,0 +1,11 @@ +#!/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 + +source $DRIVERS_TOOLS/.evergreen/auth_oidc/secrets-export.sh +export PROJECT_DIRECTORY=$PROJECT_DIRECTORY +export ENVIRONMENT=$ENVIRONMENT +printenv +export AWS_WEB_IDENTITY_TOKEN_FILE=$OIDC_TOKEN_FILE +ls -la $OIDC_TOKEN_DIR +bash ./.evergreen/${SCRIPT} \ No newline at end of file diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh deleted file mode 100755 index 98881a0c2d2..00000000000 --- a/.evergreen/run-oidc-tests.sh +++ /dev/null @@ -1,35 +0,0 @@ -#!/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" = "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 [ -z "${AZUREOIDC_CLIENTID}" ]; then - echo "Must specify an AZUREOIDC_CLIENTID" - exit 1 - fi - MONGODB_URI="${MONGODB_URI}/?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 -else - npm run check:oidc -fi diff --git a/.evergreen/run-oidc-unified-tests.sh b/.evergreen/run-oidc-unified-tests.sh new file mode 100755 index 00000000000..051256a64f9 --- /dev/null +++ b/.evergreen/run-oidc-unified-tests.sh @@ -0,0 +1,16 @@ +#!/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 + +ENVIRONMENT=${ENVIRONMENT:-"test"} +PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."} +source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" + +if [ "$ENVIRONMENT" = "test" ]; then + export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} + export MONGODB_URI_SINGLE="${MONGODB_URI_SINGLE}&authMechanismProperties=ENVIRONMENT:test" +fi +export UTIL_CLIENT_USER=$OIDC_ADMIN_USER +export UTIL_CLIENT_PASSWORD=$OIDC_ADMIN_PWD + +npm run check:oidc-auth \ No newline at end of file diff --git a/.evergreen/setup-oidc-roles.sh b/.evergreen/setup-oidc-roles.sh deleted file mode 100644 index 6be43905cf7..00000000000 --- a/.evergreen/setup-oidc-roles.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/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 - -cd ${DRIVERS_TOOLS}/.evergreen/auth_oidc -. ./activate-authoidcvenv.sh - -${DRIVERS_TOOLS}/mongodb/bin/mongosh "mongodb://localhost:27017,localhost:27018/?replicaSet=oidc-repl0&readPreference=primary" setup_oidc.js diff --git a/package.json b/package.json index 364a7af49f6..4ab34acc6e5 100644 --- a/package.json +++ b/package.json @@ -149,7 +149,10 @@ "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-test": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_test.prose.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..0b05a89f437 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,17 @@ function getDefaultAuthMechanism(hello: Document | null): AuthMechanism { return AuthMechanism.MONGODB_CR; } -const ALLOWED_PROVIDER_NAMES: AuthMechanismProperties['PROVIDER_NAME'][] = ['aws', 'azure']; +const ALLOWED_ENVIRONMENT_NAMES: AuthMechanismProperties['ENVIRONMENT'][] = [ + 'test', + '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', @@ -46,8 +51,8 @@ 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.'; +const TOKEN_RESOURCE_MISSING_ERROR = + 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is azure or gcp.'; /** @public */ export interface AuthMechanismProperties extends Document { @@ -57,15 +62,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'; + ENVIRONMENT?: 'test' | 'azure' | 'gcp'; /** @experimental */ ALLOWED_HOSTS?: string[]; /** @experimental */ - TOKEN_AUDIENCE?: string; + TOKEN_RESOURCE?: string; + /** @experimental */ + TOKEN_CLIENT_ID?: string; } /** @public */ @@ -179,45 +186,42 @@ export class MongoCredentials { } if (this.mechanism === AuthMechanism.MONGODB_OIDC) { - if (this.username && this.mechanismProperties.PROVIDER_NAME) { + if ( + this.username && + this.mechanismProperties.ENVIRONMENT && + this.mechanismProperties.ENVIRONMENT !== 'azure' + ) { throw new MongoInvalidArgumentError( - `username and PROVIDER_NAME may not be used together for mechanism '${this.mechanism}'.` + `username and ENVIRONMENT '${this.mechanismProperties.ENVIRONMENT}' may not be used together for mechanism '${this.mechanism}'.` ); } if ( - this.mechanismProperties.PROVIDER_NAME === 'azure' && - !this.mechanismProperties.TOKEN_AUDIENCE + (this.mechanismProperties.ENVIRONMENT === 'azure' || + this.mechanismProperties.ENVIRONMENT === 'gcp') && + !this.mechanismProperties.TOKEN_RESOURCE ) { - throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); + throw new MongoAzureError(TOKEN_RESOURCE_MISSING_ERROR); } if ( - this.mechanismProperties.PROVIDER_NAME && - !ALLOWED_PROVIDER_NAMES.includes(this.mechanismProperties.PROVIDER_NAME) + this.mechanismProperties.ENVIRONMENT && + !ALLOWED_ENVIRONMENT_NAMES.includes(this.mechanismProperties.ENVIRONMENT) ) { throw new MongoInvalidArgumentError( - `Currently only a PROVIDER_NAME in ${ALLOWED_PROVIDER_NAMES.join( + `Currently only a ENVIRONMENT in ${ALLOWED_ENVIRONMENT_NAMES.join( ',' )} is supported for mechanism '${this.mechanism}'.` ); } 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.ENVIRONMENT && + !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 ENVIRONMENT, 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..e95f5579f6c 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -5,9 +5,12 @@ 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 { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; +import { AutomatedCallbackWorkflow } from './mongodb_oidc/automated_callback_workflow'; +import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow'; +import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow'; +import { HumanCallbackWorkflow } from './mongodb_oidc/human_callback_workflow'; +import type { TokenCache } from './mongodb_oidc/token_cache'; +import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow'; /** Error when credentials are missing. */ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; @@ -16,7 +19,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 +39,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 = 'test' | 'azure' | 'gcp' | 'automated_callback' | 'human_callback'; export interface Workflow { /** @@ -71,10 +72,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. */ @@ -83,20 +93,25 @@ 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('automated_callback', new AutomatedCallbackWorkflow()); +OIDC_WORKFLOWS.set('human_callback', new HumanCallbackWorkflow()); +OIDC_WORKFLOWS.set('test', new TokenMachineWorkflow()); +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 +121,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); + } } /** @@ -138,11 +157,16 @@ function getCredentials(authContext: AuthContext): MongoCredentials { * Gets either a device workflow or callback workflow. */ function getWorkflow(credentials: MongoCredentials): Workflow { - const providerName = credentials.mechanismProperties.PROVIDER_NAME; - const workflow = OIDC_WORKFLOWS.get(providerName || 'callback'); + let workflow; + if (credentials.mechanismProperties.OIDC_HUMAN_CALLBACK) { + workflow = OIDC_WORKFLOWS.get('human_callback'); + } else { + const providerName = credentials.mechanismProperties.ENVIRONMENT; + workflow = OIDC_WORKFLOWS.get(providerName || 'automated_callback'); + } if (!workflow) { throw new MongoInvalidArgumentError( - `Could not load workflow for provider ${credentials.mechanismProperties.PROVIDER_NAME}` + `Could not load workflow for provider ${credentials.mechanismProperties.ENVIRONMENT}` ); } return workflow; diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts new file mode 100644 index 00000000000..9b532b81434 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -0,0 +1,61 @@ +import { type Document } from 'bson'; + +import { MongoMissingCredentialsError } from '../../../error'; +import { type Connection } from '../../connection'; +import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; +import { type OIDCCallbackFunction } from '../mongodb_oidc'; +import { CallbackWorkflow } from './callback_workflow'; +import { type TokenCache, type TokenEntry } from './token_cache'; + +const NO_CALLBACK = 'No OIDC_CALLBACK provided for callback workflow.'; + +/** + * Class implementing behaviour for the non human callback workflow. + * @internal + */ +export class AutomatedCallbackWorkflow extends CallbackWorkflow { + /** + * Execute the OIDC callback workflow. + */ + async execute( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + const callback = getCallback(credentials.mechanismProperties); + // If there is a cached access token, try to authenticate with it. If + // authentication fails with an Authentication error (18), + // invalidate the access token, fetch a new access token, and try + // to authenticate again. + // If the server fails for any other reason, do not clear the cache. + let tokenEntry: TokenEntry; + if (cache?.hasToken()) { + tokenEntry = cache.get(); + try { + return await this.finishAuthentication( + connection, + credentials, + tokenEntry.idpServerResponse + ); + } catch (error) { + if (error.code === 18) { + cache?.remove(); + return await this.oneStepAuth(connection, credentials, callback, cache); + } else { + throw error; + } + } + } + return await this.oneStepAuth(connection, credentials, callback, cache); + } +} + +/** + * Returns the callback from the mechanism properties. + */ +export function getCallback(mechanismProperties: AuthMechanismProperties): OIDCCallbackFunction { + if (mechanismProperties.OIDC_CALLBACK) { + return mechanismProperties.OIDC_CALLBACK; + } + throw new MongoMissingCredentialsError(NO_CALLBACK); +} diff --git a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts b/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts deleted file mode 100644 index 984608d899f..00000000000 --- a/src/cmap/auth/mongodb_oidc/aws_service_workflow.ts +++ /dev/null @@ -1,29 +0,0 @@ -import * as fs from 'fs'; - -import { MongoAWSError } from '../../../error'; -import { ServiceWorkflow } from './service_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.'; - -/** - * Device workflow implementation for AWS. - * - * @internal - */ -export class AwsServiceWorkflow extends ServiceWorkflow { - constructor() { - super(); - } - - /** - * Get the token from the environment. - */ - async getToken(): Promise { - const tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - if (!tokenFile) { - throw new MongoAWSError(TOKEN_MISSING_ERROR); - } - return await fs.promises.readFile(tokenFile, 'utf8'); - } -} diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts new file mode 100644 index 00000000000..96b86b2f8f8 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -0,0 +1,70 @@ +import { MongoAzureError } from '../../../error'; +import { request } from '../../../utils'; +import type { MongoCredentials } from '../mongo_credentials'; +import { type AccessToken, MachineWorkflow } from './machine_workflow'; + +/** Base URL for getting Azure tokens. */ +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' }); + +/** Invalid endpoint result error. */ +const ENDPOINT_RESULT_ERROR = + 'Azure endpoint did not return a value with only access_token and expires_in properties'; + +/** Error for when the token audience is missing in the environment. */ +const TOKEN_RESOURCE_MISSING_ERROR = + 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is azure.'; + +/** + * Device workflow implementation for Azure. + * + * @internal + */ +export class AzureMachineWorkflow extends MachineWorkflow { + /** + * Get the token from the environment. + */ + async getToken(credentials?: MongoCredentials): Promise { + const tokenAudience = credentials?.mechanismProperties.TOKEN_RESOURCE; + const username = credentials?.username; + if (!tokenAudience) { + throw new MongoAzureError(TOKEN_RESOURCE_MISSING_ERROR); + } + const response = await getAzureTokenData(tokenAudience, username); + if (!isEndpointResultValid(response)) { + throw new MongoAzureError(ENDPOINT_RESULT_ERROR); + } + return response; + } +} + +/** + * Hit the Azure endpoint to get the token data. + */ +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('client_id', username); + } + const data = await request(url.toString(), { + json: true, + headers: AZURE_HEADERS + }); + return data as AccessToken; +} + +/** + * Determines if a result returned from the endpoint is valid. + * This means the result is not nullish, contains the access_token required field + * and the expires_in required field. + */ +function isEndpointResultValid( + token: unknown +): token is { access_token: unknown; expires_in: unknown } { + if (token == null || typeof token !== 'object') return false; + return 'access_token' in token && 'expires_in' in token; +} diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts deleted file mode 100644 index fadbf5e9fd9..00000000000 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ /dev/null @@ -1,86 +0,0 @@ -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'; - -/** Base URL for getting Azure tokens. */ -const AZURE_BASE_URL = - 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01'; - -/** Azure request headers. */ -const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); - -/** Invalid endpoint result error. */ -const ENDPOINT_RESULT_ERROR = - 'Azure endpoint did not return a value with only access_token and expires_in properties'; - -/** 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.'; - -/** - * The Azure access token format. - * @internal - */ -export interface AzureAccessToken { - access_token: string; - expires_in: number; -} - -/** - * Device workflow implementation for Azure. - * - * @internal - */ -export class AzureServiceWorkflow extends ServiceWorkflow { - cache = new AzureTokenCache(); - - /** - * Get the token from the environment. - */ - async getToken(credentials?: MongoCredentials): Promise { - const tokenAudience = credentials?.mechanismProperties.TOKEN_AUDIENCE; - 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; - } - return 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, { - json: true, - headers: AZURE_HEADERS - }); - return data as AzureAccessToken; -} - -/** - * Determines if a result returned from the endpoint is valid. - * This means the result is not nullish, contains the access_token required field - * and the expires_in required field. - */ -function isEndpointResultValid( - token: unknown -): token is { access_token: unknown; expires_in: unknown } { - if (token == null || typeof token !== 'object') return false; - return 'access_token' in token && 'expires_in' in token; -} 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 9518c9d381f..00000000000 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ /dev/null @@ -1,115 +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; - // eslint-disable-next-line github/no-then - lock = lock.then(() => callback(info, context)); - return await 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..b902c30bea9 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 { 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 { - 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, TokenEntry } 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 TIMEOUT_MS = 300000; /** Properties allowed on results of callbacks. */ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; @@ -33,18 +31,7 @@ const CALLBACK_RESULT_ERROR = * 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(); - } - +export abstract class CallbackWorkflow implements Workflow { /** * Get the document to add for speculative authentication. This also needs * to add a db field from the credentials source. @@ -55,97 +42,50 @@ 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 await this.execute(connection, credentials, cache); + } + /** * Execute the OIDC callback workflow. */ - async execute( + abstract execute( connection: Connection, credentials: MongoCredentials, - reauthenticating: boolean, + cache?: TokenCache, response?: Document + ): Promise; + + /** + * Performs the one-step authorisation flow as defined in the OIDC auth spec. + */ + protected async oneStepAuth( + connection: Connection, + credentials: MongoCredentials, + callback: OIDCCallbackFunction, + cache?: TokenCache ): 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; - } - } - } - } 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 - ); - } - return result; + const tokenEntry = await this.fetchAccessToken(connection, credentials, callback); + cache?.put(tokenEntry); + return await this.finishAuthentication(connection, credentials, tokenEntry.idpServerResponse); } /** @@ -156,11 +96,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( @@ -175,7 +114,7 @@ export class CallbackWorkflow implements Workflow { /** * Finishes the callback authentication process. */ - private async finishAuthentication( + protected async finishAuthentication( connection: Connection, credentials: MongoCredentials, tokenResult: IdPServerResponse, @@ -193,79 +132,28 @@ export class CallbackWorkflow implements Workflow { * Fetches an access token using either the request or refresh callbacks and * puts it in the cache. */ - private async fetchAccessToken( + protected async fetchAccessToken( connection: Connection, credentials: MongoCredentials, - serverInfo: IdPServerInfo, - reauthenticating: boolean, - callbackHash: string, - requestCallback: OIDCRequestFunction, - refreshCallback?: OIDCRefreshFunction - ): 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); + callback: OIDCCallbackFunction, + idpInfo?: IdPInfo + ): Promise { + const params: OIDCCallbackParams = { + timeoutContext: AbortSignal.timeout(TIMEOUT_MS), + version: OIDC_VERSION + }; + if (idpInfo) { + params.idpInfo = idpInfo; } + // With no token in the cache we use the request callback. + const result = await 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. - */ -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 })) - }; + return { idpServerResponse: result, idpInfo: idpInfo }; } - // 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 })) - }; } /** @@ -278,19 +166,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..c0b04c68e63 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -0,0 +1,41 @@ +import { MongoGCPError } from '../../../error'; +import { request } from '../../../utils'; +import { type MongoCredentials } from '../mongo_credentials'; +import { type AccessToken, 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_RESOURCE_MISSING_ERROR = + 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is gcp.'; + +export class GCPMachineWorkflow extends MachineWorkflow { + /** + * Get the token from the environment. + */ + async getToken(credentials?: MongoCredentials): Promise { + const tokenAudience = credentials?.mechanismProperties.TOKEN_RESOURCE; + if (!tokenAudience) { + throw new MongoGCPError(TOKEN_RESOURCE_MISSING_ERROR); + } + return await 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); + const data = await request(url.toString(), { + json: false, + headers: GCP_HEADERS + }); + return { access_token: data }; +} diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts new file mode 100644 index 00000000000..c75f734892b --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -0,0 +1,61 @@ +import { type Document } from 'bson'; + +import { MongoMissingCredentialsError } from '../../../error'; +import { type Connection } from '../../connection'; +import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; +import { type OIDCCallbackFunction } from '../mongodb_oidc'; +import { CallbackWorkflow } from './callback_workflow'; +import { type TokenCache, type TokenEntry } from './token_cache'; + +const NO_CALLBACK = 'No OIDC_HUMAN_CALLBACK provided for human callback workflow.'; + +/** + * Class implementing behaviour for the non human callback workflow. + * @internal + */ +export class HumanCallbackWorkflow extends CallbackWorkflow { + /** + * Execute the OIDC callback workflow. + */ + async execute( + connection: Connection, + credentials: MongoCredentials, + cache?: TokenCache + ): Promise { + const callback = getCallback(credentials.mechanismProperties); + // If there is a cached access token, try to authenticate with it. If + // authentication fails with an Authentication error (18), + // invalidate the access token, fetch a new access token, and try + // to authenticate again. + // If the server fails for any other reason, do not clear the cache. + let tokenEntry: TokenEntry; + if (cache?.hasToken()) { + tokenEntry = cache.get(); + try { + return await this.finishAuthentication( + connection, + credentials, + tokenEntry.idpServerResponse + ); + } catch (error) { + if (error.code === 18) { + cache?.remove(); + return await this.oneStepAuth(connection, credentials, callback, cache); + } else { + throw error; + } + } + } + return await this.oneStepAuth(connection, credentials, callback, cache); + } +} + +/** + * Returns a human callback from the mechanism properties. + */ +export function getCallback(mechanismProperties: AuthMechanismProperties): OIDCCallbackFunction { + if (mechanismProperties.OIDC_HUMAN_CALLBACK) { + return mechanismProperties.OIDC_HUMAN_CALLBACK; + } + throw new MongoMissingCredentialsError(NO_CALLBACK); +} 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..bd5310a397d --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -0,0 +1,83 @@ +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'; + +/** + * The access token format. + * @internal + */ +export interface AccessToken { + access_token: string; + expires_in?: number; +} + +/** + * 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 await 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 await 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 { + if (cache?.hasToken()) { + return cache.get().idpServerResponse.accessToken; + } else { + const token = await this.getToken(credentials); + cache?.put({ + idpServerResponse: { accessToken: token.access_token, expiresInSeconds: token.expires_in } + }); + return token.access_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 dcf086b8071..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 await 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..29120f70893 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/token_cache.ts @@ -0,0 +1,32 @@ +import { MongoDriverError } from '../../../error'; +import type { IdPInfo, IdPServerResponse } from '../mongodb_oidc'; + +/** @internal */ +export interface TokenEntry { + idpServerResponse: IdPServerResponse; + idpInfo?: IdPInfo; +} + +/** @internal */ +export class TokenCache { + private entry?: TokenEntry; + + hasToken(): boolean { + return !!this.entry; + } + + get(): TokenEntry { + if (!this.entry) { + throw new MongoDriverError('Requested an OIDC token entry which is not in the cache.'); + } + return this.entry; + } + + put(result: TokenEntry) { + this.entry = result; + } + + remove() { + this.entry = 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/cmap/auth/mongodb_oidc/token_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts new file mode 100644 index 00000000000..ea6eb50b830 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts @@ -0,0 +1,30 @@ +import * as fs from 'fs'; + +import { MongoAWSError } from '../../../error'; +import { type AccessToken, MachineWorkflow } from './machine_workflow'; + +/** Error for when the token is missing in the environment. */ +const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; + +/** + * Device workflow implementation for AWS. + * + * @internal + */ +export class TokenMachineWorkflow extends MachineWorkflow { + constructor() { + super(); + } + + /** + * Get the token from the environment. + */ + async getToken(): Promise { + const tokenFile = process.env.OIDC_TOKEN_FILE; + if (!tokenFile) { + throw new MongoAWSError(TOKEN_MISSING_ERROR); + } + const token = await fs.promises.readFile(tokenFile, 'utf8'); + return { access_token: token }; + } +} diff --git a/src/error.ts b/src/error.ts index 28c269af6be..5c607abfdb9 100644 --- a/src/error.ts +++ b/src/error.ts @@ -557,6 +557,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 812d045ba6a..0fd66e5d934 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, TokenEntry } 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 1e21aefe35a..aee241076f9 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -10,6 +10,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'; @@ -524,7 +525,7 @@ export class MongoClient extends TypedEventEmitter { if (options.credentials?.mechanism === AuthMechanism.MONGODB_OIDC) { const allowedHosts = options.credentials?.mechanismProperties?.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; - const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; + const isServiceAuth = !!options.credentials?.mechanismProperties?.ENVIRONMENT; if (!isServiceAuth) { for (const host of options.hosts) { if (!hostMatchesWildcards(host.toHostPort().host, allowedHosts)) { @@ -828,6 +829,8 @@ export interface MongoOptions extendedMetadata: Promise; /** @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..a27c8fcafee 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -1,25 +1,17 @@ import { expect } from 'chai'; -import { - type Collection, - type CommandFailedEvent, - type CommandStartedEvent, - type CommandSucceededEvent, - type MongoClient, - OIDC_WORKFLOWS -} from '../../mongodb'; +import { type Collection, MongoClient, type MongoClientOptions } from '../../mongodb'; -describe('OIDC Auth Spec Prose Tests', function () { - const callbackCache = OIDC_WORKFLOWS.get('callback').cache; - const azureCache = OIDC_WORKFLOWS.get('azure').cache; +const DEFAULT_URI = 'mongodb://127.0.0.1:27017'; - describe('3. Azure Automatic Auth', function () { +describe('OIDC Auth Spec Azure Tests', function () { + describe('5. Azure Tests', function () { let client: MongoClient; let collection: Collection; beforeEach(function () { - if (!this.configuration.isAzureOIDC(process.env.MONGODB_URI)) { - this.skipReason = 'Azure OIDC prose tests require an Azure OIDC environment.'; + if (!this.configuration.isOIDC(process.env.MONGODB_URI_SINGLE, 'azure')) { + this.skipReason = 'Azure OIDC tests require an Azure OIDC environment.'; this.skip(); } }); @@ -28,181 +20,68 @@ describe('OIDC Auth Spec Prose Tests', 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'); - }); - - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:. - // Assert that a find operation succeeds. + describe('5.1 Azure With No Username', function () { + // Create an OIDC configured client with ENVIRONMENT:azure and a valid TOKEN_RESOURCE and no username. + // Perform a find operation that succeeds. // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('3.2 Allowed Hosts Ignored', function () { beforeEach(function () { - client = this.configuration.newClient(process.env.MONGODB_URI, { - authMechanismProperties: { - ALLOWED_HOSTS: [] - } - }); + const options: MongoClientOptions = {}; + // if (process.env.AZUREOIDC_USERNAME) { + // options.auth = { username: process.env.AZUREOIDC_USERNAME, password: undefined }; + // } + if (process.env.AZUREOIDC_RESOURCE) { + options.authMechanismProperties = { TOKEN_RESOURCE: process.env.AZUREOIDC_RESOURCE }; + } + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, options); collection = client.db('test').collection('test'); }); - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:, - // and an ALLOWED_HOSTS that is an empty list. - // Assert that a find operation succeeds. - // 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'); - }); - - // Clear the main OIDC cache. - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:. - // Assert that a find operation succeeds. + describe('5.2 Azure With Bad Username', function () { + // Create an OIDC configured client with ENVIRONMENT:azure and a valid TOKEN_RESOURCE and a username of "bad". + // Perform a find operation that fails. // Close the client. - // 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; - }); - }); - - describe('3.4 Azure Cache is Used', function () { beforeEach(function () { - callbackCache?.clear(); - azureCache?.clear(); - client = this.configuration.newClient(process.env.MONGODB_URI); + const options: MongoClientOptions = {}; + if (process.env.AZUREOIDC_USERNAME) { + options.auth = { username: 'bad', password: undefined }; + } + if (process.env.AZUREOIDC_RESOURCE) { + options.authMechanismProperties = { TOKEN_RESOURCE: process.env.AZUREOIDC_RESOURCE }; + } + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, options); collection = client.db('test').collection('test'); }); - // Clear the Azure OIDC cache. - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:. - // Assert that a find operation succeeds. - // Close the client. - // 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); + it('does not authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error.message).to.include(/Azure endpoint/); }); }); - describe('3.5 Reauthentication Succeeds', function () { - const commandStartedEvents: CommandStartedEvent[] = []; - const commandSucceededEvents: CommandSucceededEvent[] = []; - const commandFailedEvents: CommandFailedEvent[] = []; - - const commandStartedListener = event => { - if (event.commandName === 'find') { - commandStartedEvents.push(event); - } - }; - const commandSucceededListener = event => { - if (event.commandName === 'find') { - commandSucceededEvents.push(event); + describe('5.3 Azure With Valid Username', function () { + // This prose test does not exist in the spec but the new OIDC setup scripts + // have a username in the environment so worth testing. + beforeEach(function () { + const options: MongoClientOptions = {}; + if (process.env.AZUREOIDC_USERNAME) { + options.auth = { username: process.env.AZUREOIDC_USERNAME, password: undefined }; } - }; - const commandFailedListener = event => { - if (event.commandName === 'find') { - commandFailedEvents.push(event); + if (process.env.AZUREOIDC_RESOURCE) { + options.authMechanismProperties = { TOKEN_RESOURCE: process.env.AZUREOIDC_RESOURCE }; } - }; - - const addListeners = () => { - client.on('commandStarted', commandStartedListener); - client.on('commandSucceeded', commandSucceededListener); - client.on('commandFailed', commandFailedListener); - }; - - // Sets up the fail point for the find to reauthenticate. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 - }, - data: { - failCommands: ['find'], - errorCode: 391 - } - }); - }; - - // Removes the fail point. - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; - - beforeEach(async function () { - azureCache?.clear(); - client = this.configuration.newClient(process.env.MONGODB_URI, { monitorCommands: true }); - await client.db('test').collection('test').findOne(); - addListeners(); - await setupFailPoint(); - }); - - afterEach(async function () { - await removeFailPoint(); + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, options); + collection = client.db('test').collection('test'); }); - // Clear the Azure OIDC cache. - // Create a client with 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. - // Clear the listener state if possible. - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 1 - // }, - // "data": { - // "failCommands": [ - // "find" - // ], - // "errorCode": 391 - // } - // } - // - //Note - // - //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 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 () { - await client.db('test').collection('test').findOne(); - expect(commandStartedEvents.map(event => event.commandName)).to.deep.equal([ - 'find', - 'find' - ]); - expect(commandSucceededEvents.map(event => event.commandName)).to.deep.equal(['find']); - expect(commandFailedEvents.map(event => event.commandName)).to.deep.equal(['find']); + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.not.be.null; }); }); }); 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..42b36e7f279 --- /dev/null +++ b/test/integration/auth/mongodb_oidc_gcp.prose.test.ts @@ -0,0 +1,54 @@ +import { expect } from 'chai'; + +import { type Collection, MongoClient, type MongoClientOptions } from '../../mongodb'; + +const DEFAULT_URI = 'mongodb://127.0.0.1:27017'; + +describe('OIDC Auth Spec GCP Tests', function () { + // Note there is no spec or tests for GCP yet, these are 2 scenarios based on the + // drivers tools scripts available. + describe('6. GCP Tests', function () { + let client: MongoClient; + let collection: Collection; + + beforeEach(function () { + if (!this.configuration.isOIDC(process.env.MONGODB_URI_SINGLE, 'gcp')) { + this.skipReason = 'GCP OIDC prose tests require a GCP OIDC environment.'; + this.skip(); + } + }); + + afterEach(async function () { + await client?.close(); + }); + + describe('6.1 GCP With Valid Token Resource', function () { + beforeEach(function () { + const options: MongoClientOptions = {}; + if (process.env.GCPOIDC_AUDIENCE) { + options.authMechanismProperties = { TOKEN_RESOURCE: process.env.GCPOIDC_AUDIENCE }; + } + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, options); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.not.be.null; + }); + }); + + describe('6.2 GCP With Invalid Token Resource', function () { + beforeEach(function () { + const options: MongoClientOptions = { authMechanismProperties: { TOKEN_RESOURCE: 'bad' } }; + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, options); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.not.be.null; + }); + }); + }); +}); diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts new file mode 100644 index 00000000000..2b9933ce11f --- /dev/null +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -0,0 +1,501 @@ +import { readFile } from 'node:fs/promises'; +import * as path from 'node:path'; + +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { + type Collection, + MongoClient, + type MongoDBOIDC, + type OIDCCallbackParams, + type OIDCResponse +} from '../../mongodb'; + +const DEFAULT_URI = 'mongodb://127.0.0.1:27017'; + +const createCallback = (tokenFile = 'test_user1', expiresInSeconds?: number, extraFields?: any) => { + return async (params: OIDCCallbackParams) => { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, tokenFile), { + encoding: 'utf8' + }); + // Assert the correct properties are set. + expect(params).to.have.property('timeoutContext'); + expect(params).to.have.property('version'); + return generateResult(token, expiresInSeconds, extraFields); + }; +}; + +// Generates the result the request or refresh callback returns. +const generateResult = (token: string, expiresInSeconds?: number, extraFields?: any) => { + const response: OIDCResponse = { accessToken: token }; + if (expiresInSeconds) { + response.expiresInSeconds = expiresInSeconds; + } + if (extraFields) { + return { ...response, ...extraFields }; + } + return response; +}; + +describe('OIDC Auth Spec Tests', function () { + beforeEach(function () { + if (process.env.ENVIRONMENT !== 'test') { + this.skipReason = 'GCP OIDC prose tests require a Test OIDC environment.'; + this.skip(); + } + }); + + describe('1. Callback Authentication', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('1.1 Callback is called during authentication', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Perform a find operation that succeeds. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + + describe('1.2 Callback is called once for multiple connections', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Start 10 threads and run 100 find operations in each thread that all succeed. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('only calls the callback once', async function () { + for (let i = 0; i < 100; i++) { + await collection.findOne(); + } + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + }); + + describe('2. OIDC Callback Validation', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('2.1 Valid Callback Inputs', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with an OIDC callback that validates its inputs and returns a valid access token. + // Perform a find operation that succeeds. + // Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + // IdpInfo can change, so we assert we called once and validate existence in the callback itself. + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + + describe('2.2 OIDC Callback Returns Null', function () { + const callbackSpy = sinon.spy(() => null); + // Create an OIDC configured client with an OIDC callback that returns null. + // Perform a find operation that fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); + }); + + describe('2.3 OIDC Callback Returns Missing Data', function () { + const callbackSpy = sinon.spy(() => { + return { field: 'value' }; + }); + // Create an OIDC configured client with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields. + // Perform a find operation that fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); + }); + + describe('2.4 Invalid Client Configuration with Callback', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with an OIDC callback and auth mechanism property ENVIRONMENT:test. + // Assert it returns a client configuration error. + it('fails validation', async function () { + try { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy, + ENVIRONMENT: 'test' + } + }); + } catch (error) { + expect(error).to.exist; + } + }); + }); + }); + + describe('3. Authentication Failure', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('3.1 Authentication failure with cached tokens fetch a new token and retry auth', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Poison the Client Cache with an invalid access token. + // Perform a find operation that succeeds. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC') as MongoDBOIDC; + provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + + describe('3.2 Authentication failures without cached tokens return an error', function () { + const callbackSpy = sinon.spy(() => { + return { accessToken: 'bad' }; + }); + // Create an OIDC configured client with an OIDC callback that always returns invalid access tokens. + // Perform a find operation that fails. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC') as MongoDBOIDC; + provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + + describe('3.3 Unexpected error code does not clear the cache', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create a MongoClient with a callback that returns a valid token. + // Set a fail point for saslStart commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "saslStart" + // ], + // errorCode: 20 // IllegalOperation + // } + // } + // Perform a find operation that fails. + // Assert that the callback has been called once. + // Perform a find operation that succeeds. + // Assert that the callback has been called once. + // Close the client. + beforeEach(async function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['saslStart'], + errorCode: 20 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('successfully authenticates', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledOnce; + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + }); + }); + }); + + describe('4. Reauthentication', function () { + let client: MongoClient; + let collection: Collection; + let callbackCount = 0; + + afterEach(async function () { + callbackCount = 0; + await client?.close(); + }); + + const createBadCallback = () => { + return async () => { + if (callbackCount === 0) { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { + encoding: 'utf8' + }); + callbackCount++; + return generateResult(token); + } + return generateResult('bad'); + }; + }; + + describe('4.1 Reauthentication Succeeds', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that succeeds. + // Assert that the callback was called 2 times (once during the connection handshake, and again during reauthentication). + // Close the client. + beforeEach(async function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + }); + }); + + describe('4.2 Read Commands Fail If Reauthentication Fails', function () { + const callbackSpy = sinon.spy(createBadCallback()); + // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. + // Perform a find operation that succeeds. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that fails. + // Assert that the callback was called 2 times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledTwice; + }); + }); + + describe('4.3 Write Commands Fail If Reauthentication Fails', function () { + const callbackSpy = sinon.spy(createBadCallback()); + // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. + // Perform an insert operation that succeeds. + // Set a fail point for insert commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "insert" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform an insert operation that fails. + // Assert that the callback was called 2 times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await collection.insertOne({ n: 1 }); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['insert'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.insertOne({ n: 2 }).catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledTwice; + }); + }); + }); +}); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts index bb4cfcb671f..2acac7c2a40 100644 --- a/test/manual/mongodb_oidc.prose.test.ts +++ b/test/manual/mongodb_oidc.prose.test.ts @@ -9,66 +9,41 @@ 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 () { +describe('OIDC Auth Spec Prose Tests', function () { context('when running in the environment', function () { it('contains AWS_WEB_IDENTITY_TOKEN_FILE', function () { expect(process.env).to.have.property('AWS_WEB_IDENTITY_TOKEN_FILE'); }); }); - 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; + describe('1. Callback Authentication', function () { // 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_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_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_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_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_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_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_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_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 () { @@ -376,12 +262,12 @@ describe('MONGODB-OIDC', function () { describe('2.1 Single Principal', function () { before(function () { client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT: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. + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws. // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { @@ -393,12 +279,12 @@ describe('MONGODB-OIDC', function () { describe('2.2 Multiple Principal User 1', function () { before(function () { client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT: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. + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred. // Perform a find operation that succeeds. // Close the client. it('successfully authenticates', async function () { @@ -417,9 +303,9 @@ describe('MONGODB-OIDC', function () { 'test_user2' ); client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' + 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred' ); - collection = client.db('test').collection('test'); + collection = client.db('test').collection('nodeOidcTest'); }); after(function () { @@ -427,7 +313,7 @@ describe('MONGODB-OIDC', function () { }); // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred. + // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred. // Perform a find operation that succeeds. // Close the client. // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. @@ -440,17 +326,17 @@ describe('MONGODB-OIDC', function () { describe('2.4 Allowed Hosts Ignored', function () { before(function () { client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', { authMechanismProperties: { ALLOWED_HOSTS: [] } } ); - 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. + // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws, and an ALLOWED_HOSTS that is an empty list. // Assert that a find operation succeeds. // Close the client. it('successfully authenticates', async function () { @@ -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_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_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_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_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_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_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_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_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 d6c78208695..2d44f357863 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -107,13 +107,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/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/mongodb_oidc/token_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..42b13ff9774 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -480,26 +480,10 @@ "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"], + "description": "should recognise the mechanism with aws provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws", "valid": true, "credential": { "username": null, @@ -507,14 +491,13 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true + "ENVIRONMENT": "aws" } } }, { - "description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest", "oidcRefresh"], + "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=ENVIRONMENT:aws", "valid": true, "credential": { "username": null, @@ -522,83 +505,31 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true, - "REFRESH_TOKEN_CALLBACK": true + "ENVIRONMENT": "aws" } } }, { - "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)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws", - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "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)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws", - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "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": 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=ENVIRONMENT: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&ENVIRONMENT: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=ENVIRONMENT: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..7e11c5678c8 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -350,36 +350,8 @@ 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 +- description: should recognise the mechanism with aws provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws valid: true credential: username: @@ -387,22 +359,9 @@ tests: 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) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws + ENVIRONMENT: aws +- 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=ENVIRONMENT:aws valid: true credential: username: @@ -410,47 +369,24 @@ tests: source: "$external" 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) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=PROVIDER_NAME:aws - valid: true - credential: - username: - password: - source: "$external" - 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 + ENVIRONMENT: aws +- description: should throw an exception if supplied a password (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT: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&ENVIRONMENT: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=ENVIRONMENT: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..e7a71b255e9 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 {"ENVIRONMENT": "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..2b8e33c13ca 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 {"ENVIRONMENT": "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 ab2a4d519e4..a27790b207d 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -78,6 +78,7 @@ export class TestConfiguration { }; serverApi: ServerApi; activeResources: number; + isSrv: boolean; constructor(private uri: string, private context: Record) { const url = new ConnectionString(uri); @@ -92,6 +93,7 @@ export class TestConfiguration { this.topologyType = this.isLoadBalanced ? TopologyType.LoadBalanced : context.topologyType; this.buildInfo = context.buildInfo; this.serverApi = context.serverApi; + this.isSrv = uri.indexOf('mongodb+srv') > -1; this.options = { hosts, hostAddresses, @@ -159,8 +161,9 @@ export class TestConfiguration { return this.options.replicaSet; } - isAzureOIDC(uri: string): boolean { - return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf('PROVIDER_NAME:azure') > -1; + isOIDC(uri: string, env: string): boolean { + if (!uri) return false; + return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf(`ENVIRONMENT:${env}`) > -1; } newClient(urlOrQueryOptions?: string | Record, serverOptions?: Record) { @@ -347,6 +350,11 @@ export class TestConfiguration { url.searchParams.append('authSource', 'admin'); } + // Secrets setup for OIDC always sets the workload URI as MONGODB_URI_SINGLE. + if (process.env.MONGODB_URI_SINGLE?.includes('MONGODB-OIDC')) { + return process.env.MONGODB_URI_SINGLE; + } + 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..1db57745eef 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(), @@ -172,7 +165,7 @@ const testConfigBeforeHook = async function () { atlas: process.env.ATLAS_CONNECTIVITY != null, aws: MONGODB_URI.includes('authMechanism=MONGODB-AWS'), awsSdk: process.env.MONGODB_AWS_SDK, - azure: MONGODB_URI.includes('PROVIDER_NAME:azure'), + azure: MONGODB_URI.includes('ENVIRONMENT:azure'), adl: this.configuration.buildInfo.dataLake ? this.configuration.buildInfo.dataLake.version : false, diff --git a/test/tools/unified-spec-runner/entities.ts b/test/tools/unified-spec-runner/entities.ts index 4b7e4f55b14..673910bfa1f 100644 --- a/test/tools/unified-spec-runner/entities.ts +++ b/test/tools/unified-spec-runner/entities.ts @@ -118,8 +118,8 @@ export type SdamEvent = | ServerClosedEvent; export type LogMessage = Omit; -function getClient(address) { - return new MongoClient(`mongodb://${address}`, getEnvironmentalOptions()); +function getClient(address, isSrv?: boolean) { + return new MongoClient(`mongodb${isSrv ? '+srv' : ''}://${address}`, getEnvironmentalOptions()); } export class UnifiedMongoClient extends MongoClient { @@ -350,6 +350,13 @@ export class UnifiedMongoClient extends MongoClient { } export class FailPointMap extends Map { + isSrv: boolean; + + constructor(isSrv: boolean) { + super(); + this.isSrv = isSrv; + } + async enableFailPoint( addressOrClient: string | HostAddress | UnifiedMongoClient, failPoint: Document @@ -362,7 +369,8 @@ export class FailPointMap extends Map { } else { // create a new client address = addressOrClient.toString(); - client = getClient(address); + console.log('address', address); + client = getClient(address, this.isSrv); try { await client.connect(); } catch (error) { @@ -391,7 +399,7 @@ export class FailPointMap extends Map { if (process.env.SERVERLESS || process.env.LOAD_BALANCER) { hostAddress += '?loadBalanced=true'; } - const client = getClient(hostAddress); + const client = getClient(hostAddress, this.isSrv); try { await client.connect(); } catch (error) { @@ -462,10 +470,12 @@ const NO_INSTANCE_CHECK = ['errors', 'failures', 'events', 'successes', 'iterati export class EntitiesMap extends Map { failPoints: FailPointMap; + isSrv: boolean; - constructor(entries?: readonly (readonly [string, E])[] | null) { + constructor(isSrv: boolean, entries?: readonly (readonly [string, E])[] | null) { super(entries); - this.failPoints = new FailPointMap(); + this.isSrv = isSrv; + this.failPoints = new FailPointMap(isSrv); } mapOf(type: 'client'): EntitiesMap; @@ -481,7 +491,10 @@ export class EntitiesMap extends Map { if (!ctor) { throw new Error(`Unknown type ${type}`); } - return new EntitiesMap(Array.from(this.entries()).filter(([, e]) => e instanceof ctor)); + return new EntitiesMap( + this.isSrv, + Array.from(this.entries()).filter(([, e]) => e instanceof ctor) + ); } getChangeStreamOrCursor(key: string): UnifiedChangeStream | AbstractCursor { @@ -561,24 +574,32 @@ export class EntitiesMap extends Map { entities?: EntityDescription[], entityMap?: EntitiesMap ): Promise { - const map = entityMap ?? new EntitiesMap(); + const map = entityMap ?? new EntitiesMap(config.isSrv); for (const entity of entities ?? []) { if ('client' in entity) { const useMultipleMongoses = (config.topologyType === 'LoadBalanced' || config.topologyType === 'Sharded') && entity.client.useMultipleMongoses; - const uri = makeConnectionString( - config.url({ useMultipleMongoses }), - entity.client.uriOptions - ); + console.log('client', process.env.MONGODB_URI, process.env.MONGODB_URI_SINGLE); + let uri: string; + // For OIDC we need to ensure we use MONGODB_URI_SINGLE for the MongoClient. + if (process.env.MONGODB_URI_SINGLE?.includes('MONGODB-OIDC')) { + uri = makeConnectionString(process.env.MONGODB_URI_SINGLE, entity.client.uriOptions); + } else { + uri = makeConnectionString(config.url({ useMultipleMongoses }), entity.client.uriOptions); + } const client = new UnifiedMongoClient(uri, entity.client); + console.log(entity.client, uri); new EntityEventRegistry(client, entity.client, map).register(); try { + console.log('connecting'); await client.connect(); } catch (error) { + console.log('error', error); console.error(ejson`failed to connect entity ${entity}`); throw error; } + console.log('setting client', entity.client.id, client); map.set(entity.client.id, client); } else if ('database' in entity) { const client = map.getEntity('client', entity.database.client); diff --git a/test/tools/unified-spec-runner/runner.ts b/test/tools/unified-spec-runner/runner.ts index b49b6aa5826..4ea85a5c16c 100644 --- a/test/tools/unified-spec-runner/runner.ts +++ b/test/tools/unified-spec-runner/runner.ts @@ -73,6 +73,14 @@ 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) { + // For OIDC tests the MONGODB_URI is the base admin URI that the util client will use. + utilClient = ctx.configuration.newClient(process.env.MONGODB_URI, { + auth: { + username: process.env.UTIL_CLIENT_USER, + password: process.env.UTIL_CLIENT_PASSWORD + } + }); } else { utilClient = ctx.configuration.newClient(); } @@ -212,9 +220,11 @@ async function runUnifiedTest( // If any event listeners were enabled on any client entities, // the test runner MUST now disable those event listeners. for (const [id, client] of entities.mapOf('client')) { + console.log('id, client', id, client); client.stopCapturingEvents(); clientList.set(id, client); } + console.log('clientList', clientList); if (test.expectEvents) { for (const expectedEventsForClient of test.expectEvents) { diff --git a/test/tools/unified-spec-runner/schema.ts b/test/tools/unified-spec-runner/schema.ts index 6fceee9a6a5..ea331ce6911 100644 --- a/test/tools/unified-spec-runner/schema.ts +++ b/test/tools/unified-spec-runner/schema.ts @@ -108,6 +108,7 @@ export type TopologyName = (typeof TopologyName)[keyof typeof TopologyName]; export interface RunOnRequirement { serverless?: 'forbid' | 'allow' | 'require'; auth?: boolean; + authMechanism?: string; maxServerVersion?: string; minServerVersion?: string; topologies?: TopologyName[]; diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index 233274b2925..36868be13e3 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,27 @@ 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 (process.env.AWS_WEB_IDENTITY_TOKEN_FILE) { + // connectionString.searchParams.set(name, 'ENVIRONMENT:aws'); + // } + // if (process.env.GCPOIDC_AUDIENCE) { + // connectionString.searchParams.set( + // name, + // `ENVIRONMENT:gcp,TOKEN_RESOURCE:${process.env.GCPOIDC_AUDIENCE}` + // ); + // } + // if (process.env.AZUREOIDC_CLIENTID) { + // connectionString.searchParams.set( + // name, + // `ENVIRONMENT:azure,TOKEN_RESOURCE:${process.env.AZUREOIDC_CLIENTID}` + // ); + // } + } 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 844e5bd4705..60c6dd89ba5 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.test.ts b/test/unit/cmap/auth/mongodb_oidc.test.ts index 121244688e9..555bdb42ba9 100644 --- a/test/unit/cmap/auth/mongodb_oidc.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc.test.ts @@ -18,7 +18,7 @@ describe('class MongoDBOIDC', () => { {}, new MongoCredentials({ mechanism: 'MONGODB-OIDC', - mechanismProperties: { PROVIDER_NAME: 'iLoveJavaScript' } + mechanismProperties: { ENVIRONMENT: 'iLoveJavaScript' } }), {} ) @@ -37,7 +37,7 @@ describe('class MongoDBOIDC', () => { {}, new MongoCredentials({ mechanism: 'MONGODB-OIDC', - mechanismProperties: { PROVIDER_NAME: 'iLoveJavaScript' } + mechanismProperties: { ENVIRONMENT: 'iLoveJavaScript' } }), {} ) diff --git a/test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts deleted file mode 100644 index 55438240e7f..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc/aws_service_workflow.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; - -import { AwsServiceWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; - -describe('AwsDeviceWorkFlow', function () { - describe('#execute', function () { - const workflow = new AwsServiceWorkflow(); - - context('when AWS_WEB_IDENTITY_TOKEN_FILE is not in the env', function () { - let file; - const connection = sinon.createStubInstance(Connection); - const credentials = sinon.createStubInstance(MongoCredentials); - - before(function () { - file = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - delete process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - }); - - after(function () { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = file; - }); - - it('throws an error', async function () { - try { - await workflow.execute(connection, credentials); - expect.fail('workflow must fail without AWS_WEB_IDENTITY_TOKEN_FILE'); - } catch (error) { - expect(error.message).to.include('AWS_WEB_IDENTITY_TOKEN_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..68bcb71bc90 --- /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_RESOURCE 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_RESOURCE'); + } catch (error) { + expect(error.message).to.include('TOKEN_RESOURCE'); + } + }); + }); + }); +}); 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..73ac6c7869f --- /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_RESOURCE 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_RESOURCE'); + } catch (error) { + expect(error.message).to.include('TOKEN_RESOURCE'); + } + }); + }); + }); +}); 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); - }); - }); - }); -}); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts new file mode 100644 index 00000000000..33ea4960ca2 --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -0,0 +1,34 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import { Connection, MongoCredentials, TokenMachineWorkflow } from '../../../../mongodb'; + +describe('TokenMachineFlow', function () { + describe('#execute', function () { + const workflow = new TokenMachineWorkflow(); + + context('when OIDC_TOKEN_FILE is not in the env', function () { + let file; + const connection = sinon.createStubInstance(Connection); + const credentials = sinon.createStubInstance(MongoCredentials); + + before(function () { + file = process.env.OIDC_TOKEN_FILE; + delete process.env.OIDC_TOKEN_FILE; + }); + + after(function () { + process.env.OIDC_TOKEN_FILE = file; + }); + + it('throws an error', async function () { + try { + await workflow.execute(connection, credentials); + expect.fail('workflow must fail without OIDC_TOKEN_FILE'); + } catch (error) { + expect(error.message).to.include('OIDC_TOKEN_FILE'); + } + }); + }); + }); +}); diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 2a38fc491ad..a1bd6f1f603 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -303,7 +303,7 @@ describe('Connection String', function () { it('raises an error', function () { expect(() => { parseOptions( - 'mongodb://localhost/?authMechanismProperties=PROVIDER_NAME:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' + 'mongodb://localhost/?authMechanismProperties=ENVIRONMENT:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' ); }).to.throw( MongoParseError, @@ -318,7 +318,7 @@ describe('Connection String', function () { it('sets the allowed hosts property', function () { const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', { authMechanismProperties: { ALLOWED_HOSTS: hosts @@ -326,7 +326,7 @@ describe('Connection String', function () { } ); expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'aws', + ENVIRONMENT: 'aws', ALLOWED_HOSTS: hosts }); }); @@ -336,7 +336,7 @@ describe('Connection String', function () { it('raises an error', function () { expect(() => { parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', { authMechanismProperties: { ALLOWED_HOSTS: [1, 2, 3] @@ -354,25 +354,25 @@ describe('Connection String', function () { context('when ALLOWED_HOSTS is not in the options', function () { it('sets the default value', function () { const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws' + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws' ); expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'aws', + ENVIRONMENT: 'aws', ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS }); }); }); - context('when TOKEN_AUDIENCE is in the properties', function () { + context('when TOKEN_RESOURCE is in the properties', function () { context('when it is a uri', function () { const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:api%3A%2F%2Ftest' + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:api%3A%2F%2Ftest' ); it('parses the uri', function () { expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'azure', - TOKEN_AUDIENCE: 'api://test', + ENVIRONMENT: 'azure', + TOKEN_RESOURCE: 'api://test', ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS }); }); @@ -655,7 +655,7 @@ describe('Connection String', function () { makeStub('authSource=thisShouldNotBeAuthSource'); const mechanismProperties = {}; if (mechanism === AuthMechanism.MONGODB_OIDC) { - mechanismProperties.PROVIDER_NAME = 'aws'; + mechanismProperties.ENVIRONMENT = 'aws'; } const credentials = new MongoCredentials({ diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 508f3d85c2a..726219ca1ad 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -86,6 +86,7 @@ const EXPECTED_EXPORTS = [ 'MongoError', 'MongoErrorLabel', 'MongoExpiredSessionError', + 'MongoGCPError', 'MongoGridFSChunkError', 'MongoGridFSStreamError', 'MongoInvalidArgumentError', @@ -124,6 +125,7 @@ const EXPECTED_EXPORTS = [ 'ServerType', 'SrvPollingEvent', 'Timestamp', + 'TokenCache', 'TopologyClosedEvent', 'TopologyDescriptionChangedEvent', 'TopologyOpeningEvent', From 43016d7eb07bc2154b465f010d93247ec7a3f926 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sat, 27 Apr 2024 20:58:10 +0200 Subject: [PATCH 02/64] test: migrate human callback tests part one --- .evergreen/run-oidc-prose-tests.sh | 2 +- .../automated_callback_workflow.ts | 3 + .../auth/mongodb_oidc_test.prose.test.ts | 996 +++++++++++------- 3 files changed, 640 insertions(+), 361 deletions(-) diff --git a/.evergreen/run-oidc-prose-tests.sh b/.evergreen/run-oidc-prose-tests.sh index 51e4bf00afe..ee0e1afc568 100755 --- a/.evergreen/run-oidc-prose-tests.sh +++ b/.evergreen/run-oidc-prose-tests.sh @@ -2,7 +2,7 @@ set -o errexit # Exit the script with error if any of the commands fail set -o xtrace # Write all commands first to stderr -ENVIRONMENT=${ENVIRONMENT:-"aws"} +ENVIRONMENT=${ENVIRONMENT:-"test"} PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."} source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 9b532b81434..b5b754500bd 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -31,6 +31,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { let tokenEntry: TokenEntry; if (cache?.hasToken()) { tokenEntry = cache.get(); + console.log(tokenEntry); try { return await this.finishAuthentication( connection, @@ -38,6 +39,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { tokenEntry.idpServerResponse ); } catch (error) { + console.log(error); if (error.code === 18) { cache?.remove(); return await this.oneStepAuth(connection, credentials, callback, cache); @@ -46,6 +48,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } } } + console.log('no token, one step'); return await this.oneStepAuth(connection, credentials, callback, cache); } } diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index 2b9933ce11f..25821009560 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -12,8 +12,6 @@ import { type OIDCResponse } from '../../mongodb'; -const DEFAULT_URI = 'mongodb://127.0.0.1:27017'; - const createCallback = (tokenFile = 'test_user1', expiresInSeconds?: number, extraFields?: any) => { return async (params: OIDCCallbackParams) => { const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, tokenFile), { @@ -46,455 +44,733 @@ describe('OIDC Auth Spec Tests', function () { } }); - describe('1. Callback Authentication', function () { - let client: MongoClient; - let collection: Collection; + describe('Machine Authentication Flow Prose Tests', function () { + const uriSingle = process.env.MONGODB_URI_SINGLE; - afterEach(async function () { - await client?.close(); - }); + describe('1. Callback Authentication', function () { + let client: MongoClient; + let collection: Collection; - describe('1.1 Callback is called during authentication', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client. - // Perform a find operation that succeeds. - // Assert that the callback was called 1 time. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } - }); - collection = client.db('test').collection('test'); + afterEach(async function () { + await client?.close(); }); - it('successfully authenticates', async function () { - await collection.findOne(); - expect(callbackSpy).to.have.been.calledOnce; - }); - }); + describe('1.1 Callback is called during authentication', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Perform a find operation that succeeds. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); - describe('1.2 Callback is called once for multiple connections', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client. - // Start 10 threads and run 100 find operations in each thread that all succeed. - // Assert that the callback was called 1 time. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; }); - collection = client.db('test').collection('test'); }); - it('only calls the callback once', async function () { - for (let i = 0; i < 100; i++) { - await collection.findOne(); - } - expect(callbackSpy).to.have.been.calledOnce; + describe('1.2 Callback is called once for multiple connections', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Start 10 threads and run 100 find operations in each thread that all succeed. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('only calls the callback once', async function () { + for (let i = 0; i < 100; i++) { + await collection.findOne(); + } + expect(callbackSpy).to.have.been.calledOnce; + }); }); }); - }); - describe('2. OIDC Callback Validation', function () { - let client: MongoClient; - let collection: Collection; + describe('2. OIDC Callback Validation', function () { + let client: MongoClient; + let collection: Collection; - afterEach(async function () { - await client?.close(); - }); + afterEach(async function () { + await client?.close(); + }); - describe('2.1 Valid Callback Inputs', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client with an OIDC callback that validates its inputs and returns a valid access token. - // Perform a find operation that succeeds. - // Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + describe('2.1 Valid Callback Inputs', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with an OIDC callback that validates its inputs and returns a valid access token. + // Perform a find operation that succeeds. + // Assert that the OIDC callback was called with the appropriate inputs, including the timeout parameter if possible. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + // IdpInfo can change, so we assert we called once and validate existence in the callback itself. + expect(callbackSpy).to.have.been.calledOnce; }); - collection = client.db('test').collection('test'); }); - it('successfully authenticates', async function () { - await collection.findOne(); - // IdpInfo can change, so we assert we called once and validate existence in the callback itself. - expect(callbackSpy).to.have.been.calledOnce; + describe('2.2 OIDC Callback Returns Null', function () { + const callbackSpy = sinon.spy(() => null); + // Create an OIDC configured client with an OIDC callback that returns null. + // Perform a find operation that fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); }); - }); - describe('2.2 OIDC Callback Returns Null', function () { - const callbackSpy = sinon.spy(() => null); - // Create an OIDC configured client with an OIDC callback that returns null. - // Perform a find operation that fails. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + describe('2.3 OIDC Callback Returns Missing Data', function () { + const callbackSpy = sinon.spy(() => { + return { field: 'value' }; + }); + // Create an OIDC configured client with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields. + // Perform a find operation that fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; }); - collection = client.db('test').collection('test'); }); - it('does not successfully authenticate', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.exist; + describe('2.4 Invalid Client Configuration with Callback', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with an OIDC callback and auth mechanism property ENVIRONMENT:test. + // Assert it returns a client configuration error. + it('fails validation', async function () { + try { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy, + ENVIRONMENT: 'test' + } + }); + } catch (error) { + expect(error).to.exist; + } + }); }); }); - describe('2.3 OIDC Callback Returns Missing Data', function () { - const callbackSpy = sinon.spy(() => { - return { field: 'value' }; + describe('3. Authentication Failure', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); }); - // Create an OIDC configured client with an OIDC callback that returns data not conforming to the OIDCCredential with missing fields. - // Perform a find operation that fails. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + + describe('3.1 Authentication failure with cached tokens fetch a new token and retry auth', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Poison the Client Cache with an invalid access token. + // Perform a find operation that succeeds. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + const provider = client.s.authProviders.getOrCreateProvider( + 'MONGODB-OIDC' + ) as MongoDBOIDC; + provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + collection = client.db('test').collection('test'); }); - collection = client.db('test').collection('test'); - }); - it('does not successfully authenticate', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.exist; + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + }); }); - }); - describe('2.4 Invalid Client Configuration with Callback', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client with an OIDC callback and auth mechanism property ENVIRONMENT:test. - // Assert it returns a client configuration error. - it('fails validation', async function () { - try { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { + describe('3.2 Authentication failures without cached tokens return an error', function () { + const callbackSpy = sinon.spy(() => { + return { accessToken: 'bad' }; + }); + // Create an OIDC configured client with an OIDC callback that always returns invalid access tokens. + // Perform a find operation that fails. + // Assert that the callback was called 1 time. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { authMechanismProperties: { - OIDC_CALLBACK: callbackSpy, - ENVIRONMENT: 'test' + OIDC_CALLBACK: callbackSpy } }); - } catch (error) { + const provider = client.s.authProviders.getOrCreateProvider( + 'MONGODB-OIDC' + ) as MongoDBOIDC; + provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + collection = client.db('test').collection('test'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); expect(error).to.exist; - } + expect(callbackSpy).to.have.been.calledOnce; + }); }); - }); - }); - - describe('3. Authentication Failure', function () { - let client: MongoClient; - let collection: Collection; - afterEach(async function () { - await client?.close(); - }); + describe('3.3 Unexpected error code does not clear the cache', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create a MongoClient with a callback that returns a valid token. + // Set a fail point for saslStart commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "saslStart" + // ], + // errorCode: 20 // IllegalOperation + // } + // } + // Perform a find operation that fails. + // Assert that the callback has been called once. + // Perform a find operation that succeeds. + // Assert that the callback has been called once. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['saslStart'], + errorCode: 20 + } + }); + }); - describe('3.1 Authentication failure with cached tokens fetch a new token and retry auth', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client. - // Poison the Client Cache with an invalid access token. - // Perform a find operation that succeeds. - // Assert that the callback was called 1 time. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); }); - const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC') as MongoDBOIDC; - provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); - collection = client.db('test').collection('test'); - }); - it('successfully authenticates', async function () { - await collection.findOne(); - expect(callbackSpy).to.have.been.calledOnce; + it('successfully authenticates the second time', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledOnce; + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + }); }); }); - describe('3.2 Authentication failures without cached tokens return an error', function () { - const callbackSpy = sinon.spy(() => { - return { accessToken: 'bad' }; + describe('4. Reauthentication', function () { + let client: MongoClient; + let collection: Collection; + let callbackCount = 0; + + afterEach(async function () { + callbackCount = 0; + await client?.close(); }); - // Create an OIDC configured client with an OIDC callback that always returns invalid access tokens. - // Perform a find operation that fails. - // Assert that the callback was called 1 time. - // Close the client. - beforeEach(function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy + + const createBadCallback = () => { + return async () => { + if (callbackCount === 0) { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { + encoding: 'utf8' + }); + callbackCount++; + return generateResult(token); } + return generateResult('bad'); + }; + }; + + describe('4.1 Reauthentication Succeeds', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that succeeds. + // Assert that the callback was called 2 times (once during the connection handshake, and again during reauthentication). + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); }); - const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC') as MongoDBOIDC; - provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); - collection = client.db('test').collection('test'); - }); - it('does not successfully authenticate', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.exist; - expect(callbackSpy).to.have.been.calledOnce; + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + }); }); - }); - describe('3.3 Unexpected error code does not clear the cache', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create a MongoClient with a callback that returns a valid token. - // Set a fail point for saslStart commands of the form: - // { - // configureFailPoint: "failCommand", - // mode: { - // times: 1 - // }, - // data: { - // failCommands: [ - // "saslStart" - // ], - // errorCode: 20 // IllegalOperation - // } - // } - // Perform a find operation that fails. - // Assert that the callback has been called once. - // Perform a find operation that succeeds. - // Assert that the callback has been called once. - // Close the client. - beforeEach(async function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + describe('4.2 Read Commands Fail If Reauthentication Fails', function () { + const callbackSpy = sinon.spy(createBadCallback()); + // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. + // Perform a find operation that succeeds. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that fails. + // Assert that the callback was called 2 times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); }); - collection = client.db('test').collection('test'); - await client - .db() - .admin() - .command({ + + afterEach(async function () { + await client.db().admin().command({ configureFailPoint: 'failCommand', - mode: { - times: 1 - }, - data: { - failCommands: ['saslStart'], - errorCode: 20 - } + mode: 'off' }); - }); + }); - afterEach(async function () { - await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledTwice; }); }); - it('successfully authenticates', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.exist; - expect(callbackSpy).to.have.been.calledOnce; - await collection.findOne(); - expect(callbackSpy).to.have.been.calledOnce; + describe('4.3 Write Commands Fail If Reauthentication Fails', function () { + const callbackSpy = sinon.spy(createBadCallback()); + // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. + // Perform an insert operation that succeeds. + // Set a fail point for insert commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "insert" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform an insert operation that fails. + // Assert that the callback was called 2 times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('test'); + await collection.insertOne({ n: 1 }); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['insert'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.insertOne({ n: 2 }).catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledTwice; + }); }); }); }); - describe('4. Reauthentication', function () { - let client: MongoClient; - let collection: Collection; - let callbackCount = 0; + describe('Human Authentication Flow Prose Tests', function () { + const uriSingle = process.env.MONGODB_URI_SINGLE; + const uriMulti = process.env.MONGODB_URI_MULTI; - afterEach(async function () { - callbackCount = 0; - await client?.close(); - }); + describe('1. OIDC Human Callback Authentication', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); - const createBadCallback = () => { - return async () => { - if (callbackCount === 0) { - const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { - encoding: 'utf8' + describe('1.1 Single Principal Implicit Username', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client. + // Perform a find operation that succeeds. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } }); - callbackCount++; - return generateResult(token); - } - return generateResult('bad'); - }; - }; - - describe('4.1 Reauthentication Succeeds', function () { - const callbackSpy = sinon.spy(createCallback()); - // Create an OIDC configured client. - // Set a fail point for find commands of the form: - // { - // configureFailPoint: "failCommand", - // mode: { - // times: 1 - // }, - // data: { - // failCommands: [ - // "find" - // ], - // errorCode: 391 // ReauthenticationRequired - // } - // } - // Perform a find operation that succeeds. - // Assert that the callback was called 2 times (once during the connection handshake, and again during reauthentication). - // Close the client. - beforeEach(async function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + collection = client.db('test').collection('testHuman'); }); - collection = client.db('test').collection('test'); - await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; + }); + }); + + describe('1.2 Single Principal Explicit Username', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with MONGODB_URI_SINGLE and a username of test_user1@${OIDC_DOMAIN}. + // Perform a find operation that succeeds. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + auth: { + username: `test_user1@${process.env.OIDC_DOMAIN}`, + password: undefined }, - data: { - failCommands: ['find'], - errorCode: 391 + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy } }); - }); + collection = client.db('test').collection('testHuman'); + }); - afterEach(async function () { - await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; }); }); - it('successfully authenticates', async function () { - await collection.findOne(); - expect(callbackSpy).to.have.been.calledTwice; - }); - }); + describe('1.3 Multiple Principal User 1', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with MONGODB_URI_MULTI and username of test_user1@${OIDC_DOMAIN}. + // Perform a find operation that succeeds. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriMulti, { + auth: { + username: `test_user1@${process.env.OIDC_DOMAIN}`, + password: undefined + }, + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('testHuman'); + }); - describe('4.2 Read Commands Fail If Reauthentication Fails', function () { - const callbackSpy = sinon.spy(createBadCallback()); - // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. - // Perform a find operation that succeeds. - // Set a fail point for find commands of the form: - // { - // configureFailPoint: "failCommand", - // mode: { - // times: 1 - // }, - // data: { - // failCommands: [ - // "find" - // ], - // errorCode: 391 // ReauthenticationRequired - // } - // } - // Perform a find operation that fails. - // Assert that the callback was called 2 times. - // Close the client. - beforeEach(async function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; }); - collection = client.db('test').collection('test'); - await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 + }); + + describe('1.4 Multiple Principal User 2', function () { + const callbackSpy = sinon.spy(createCallback('test_user2')); + // Create an OIDC configured client with MONGODB_URI_MULTI and username of test_user2@${OIDC_DOMAIN}. that reads the test_user2 token file. + // Perform a find operation that succeeds. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriMulti, { + auth: { + username: `test_user2@${process.env.OIDC_DOMAIN}`, + password: undefined }, - data: { - failCommands: ['find'], - errorCode: 391 + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy } }); + collection = client.db('test').collection('testHuman'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; + }); }); - afterEach(async function () { - await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' + describe('1.5 Multiple Principal No User', function () { + const callbackSpy = sinon.spy(createCallback(null)); + // Create an OIDC configured client with MONGODB_URI_MULTI and no username. + // Assert that a find operation fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriMulti, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('testHuman'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; }); }); - it('does not successfully authenticate', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.exist; - expect(callbackSpy).to.have.been.calledTwice; + describe('1.6 Allowed Hosts Blocked', function () { + context('when provided an empty ALLOWED_HOSTS', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with an ALLOWED_HOSTS that is an empty list. + // Assert that a find operation fails with a client-side error. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy, + ALLOWED_HOSTS: [] + } + }); + collection = client.db('test').collection('testHuman'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); + }); + + context('when provided invalid ALLOWED_HOSTS', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create a client that uses the URL mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com, + // a human callback, and an ALLOWED_HOSTS that contains ["example.com"]. + // Assert that a find operation fails with a client-side error. + // Close the client. + // NOTE: For Node we remove the ignored=example.com URI option as we error on unrecognised options. + beforeEach(function () { + client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy, + ALLOWED_HOSTS: ['example.com'] + } + }); + collection = client.db('test').collection('testHuman'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); + }); }); - }); - describe('4.3 Write Commands Fail If Reauthentication Fails', function () { - const callbackSpy = sinon.spy(createBadCallback()); - // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. - // Perform an insert operation that succeeds. - // Set a fail point for insert commands of the form: - // { - // configureFailPoint: "failCommand", - // mode: { - // times: 1 - // }, - // data: { - // failCommands: [ - // "insert" - // ], - // errorCode: 391 // ReauthenticationRequired - // } - // } - // Perform an insert operation that fails. - // Assert that the callback was called 2 times. - // Close the client. - beforeEach(async function () { - client = new MongoClient(process.env.MONGODB_URI_SINGLE ?? DEFAULT_URI, { - authMechanismProperties: { - OIDC_CALLBACK: callbackSpy - } + describe('1.7 Allowed Hosts in Connection String Ignored', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with the connection string: + // mongodb+srv://example.com/?authMechanism=MONGODB-OIDC&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D and a Human Callback. + // Assert that the creation of the client raises a configuration error. + it('fails on client creation', async function () { + expect(() => { + new MongoClient( + `${uriSingle}&authMechanismProperties=ALLOWED_HOSTS:%5B%22example.com%22%5D`, + { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } + } + ); + }).to.throw(); }); - collection = client.db('test').collection('test'); - await collection.insertOne({ n: 1 }); - await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 + }); + + describe('1.8 Machine IdP with Human Callback', function () { + const callbackSpy = sinon.spy(createCallback('test_machine')); + // This test MUST only be run when OIDC_IS_LOCAL is set. This indicates that the server is local and not using Atlas. + // In this case, MONGODB_URI_SINGLE will be configured with a human user test_user1, and a machine user test_machine. + // This test uses the machine user with a human callback, ensuring that the missing clientId in the PrincipalStepRequest + // response is handled by the driver. + // Create an OIDC configured client with MONGODB_URI_SINGLE and a username of test_machine that uses the test_machine token. + // Perform a find operation that succeeds. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + auth: { + username: `test_machine${process.env.OIDC_DOMAIN}`, + password: undefined }, - data: { - failCommands: ['insert'], - errorCode: 391 + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy } }); + collection = client.db('test').collection('testHuman'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; + }); }); + }); + + describe('1. OIDC Human Callback Validation', function () { + let client: MongoClient; + let collection: Collection; afterEach(async function () { - await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' + await client?.close(); + }); + + describe('2.1 Valid Callback Inputs', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with a human callback that validates its inputs and returns a valid access token. + // Perform a find operation that succeeds. Verify that the human callback was called with the appropriate inputs, including the timeout parameter if possible. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('testHuman'); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; }); }); - it('does not successfully authenticate', async function () { - const error = await collection.insertOne({ n: 2 }).catch(error => error); - expect(error).to.exist; - expect(callbackSpy).to.have.been.calledTwice; + describe('2.2 Human Callback Returns Missing Data', function () { + const callbackSpy = sinon.spy(() => { + return { field: 'value' }; + }); + // Create an OIDC configured client with a human callback that returns data not conforming to the OIDCCredential with missing fields. + // Perform a find operation that fails. + // Close the client. + beforeEach(function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + } + }); + collection = client.db('test').collection('testHuman'); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); }); }); }); From f2d6ec7b581fe21899d19593bab07faecf7f8e4f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 28 Apr 2024 19:00:04 +0200 Subject: [PATCH 03/64] test: migrate human callback tests part two --- src/cmap/auth/mongodb_oidc.ts | 15 +- .../automated_callback_workflow.ts | 68 +++- .../auth/mongodb_oidc/callback_workflow.ts | 84 +--- .../mongodb_oidc/human_callback_workflow.ts | 130 +++++-- .../auth/mongodb_oidc/machine_workflow.ts | 18 +- src/cmap/auth/mongodb_oidc/token_cache.ts | 64 ++- src/index.ts | 2 +- .../auth/mongodb_oidc_azure.prose.test.ts | 3 - .../auth/mongodb_oidc_test.prose.test.ts | 367 ++++++++++++++++-- 9 files changed, 586 insertions(+), 165 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index e95f5579f6c..61af1983b9d 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -62,6 +62,9 @@ export interface OIDCCallbackParams { */ export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise; +/** The current version of OIDC implementation. */ +export const OIDC_VERSION = 1; + type ProviderName = 'test' | 'azure' | 'gcp' | 'automated_callback' | 'human_callback'; export interface Workflow { @@ -72,7 +75,7 @@ export interface Workflow { execute( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache, + cache: TokenCache, response?: Document ): Promise; @@ -82,13 +85,13 @@ export interface Workflow { reauthenticate( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise; /** * Get the document to add for speculative authentication. */ - speculativeAuth(credentials: MongoCredentials): Promise; + speculativeAuth(credentials: MongoCredentials, cache: TokenCache): Promise; } /** @internal */ @@ -104,12 +107,12 @@ OIDC_WORKFLOWS.set('gcp', new GCPMachineWorkflow()); * @experimental */ export class MongoDBOIDC extends AuthProvider { - cache?: TokenCache; + cache: TokenCache; /** * Instantiate the auth provider. */ - constructor(cache?: TokenCache) { + constructor(cache: TokenCache) { super(); this.cache = cache; } @@ -137,7 +140,7 @@ export class MongoDBOIDC extends AuthProvider { ): Promise { const credentials = getCredentials(authContext); const workflow = getWorkflow(credentials); - const result = await workflow.speculativeAuth(credentials); + const result = await workflow.speculativeAuth(credentials, this.cache); return { ...handshakeDoc, ...result }; } } diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index b5b754500bd..eeb7a5ecbdc 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -3,9 +3,14 @@ import { type Document } from 'bson'; import { MongoMissingCredentialsError } from '../../../error'; import { type Connection } from '../../connection'; import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; -import { type OIDCCallbackFunction } from '../mongodb_oidc'; -import { CallbackWorkflow } from './callback_workflow'; -import { type TokenCache, type TokenEntry } from './token_cache'; +import { + OIDC_VERSION, + type OIDCCallbackFunction, + type OIDCCallbackParams, + type OIDCResponse +} from '../mongodb_oidc'; +import { AUTOMATED_TIMEOUT_MS, CallbackWorkflow } from './callback_workflow'; +import { type TokenCache } from './token_cache'; const NO_CALLBACK = 'No OIDC_CALLBACK provided for callback workflow.'; @@ -14,13 +19,35 @@ const NO_CALLBACK = 'No OIDC_CALLBACK provided for callback workflow.'; * @internal */ export class AutomatedCallbackWorkflow extends CallbackWorkflow { + /** + * 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.removeAccessToken(); + return await this.execute(connection, credentials, cache); + } + /** * Execute the OIDC callback workflow. */ async execute( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise { const callback = getCallback(credentials.mechanismProperties); // If there is a cached access token, try to authenticate with it. If @@ -28,28 +55,33 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { // invalidate the access token, fetch a new access token, and try // to authenticate again. // If the server fails for any other reason, do not clear the cache. - let tokenEntry: TokenEntry; - if (cache?.hasToken()) { - tokenEntry = cache.get(); - console.log(tokenEntry); + if (cache.hasAccessToken) { + const token = cache.getAccessToken(); try { - return await this.finishAuthentication( - connection, - credentials, - tokenEntry.idpServerResponse - ); + return await this.finishAuthentication(connection, credentials, token); } catch (error) { - console.log(error); if (error.code === 18) { - cache?.remove(); - return await this.oneStepAuth(connection, credentials, callback, cache); + cache.removeAccessToken(); + return await this.execute(connection, credentials, cache); } else { throw error; } } } - console.log('no token, one step'); - return await this.oneStepAuth(connection, credentials, callback, cache); + const response = await this.fetchAccessToken(callback); + cache.put(response); + return await this.finishAuthentication(connection, credentials, response.accessToken); + } + + /** + * Fetches the access token using the callback. + */ + protected async fetchAccessToken(callback: OIDCCallbackFunction): Promise { + const params: OIDCCallbackParams = { + timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), + version: OIDC_VERSION + }; + return await this.executeAndValidateCallback(callback, params); } } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index b902c30bea9..05adc2707a2 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -4,21 +4,19 @@ import { MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; -import type { - IdPInfo, - IdPServerResponse, - OIDCCallbackFunction, - OIDCCallbackParams, - Workflow +import { + type OIDCCallbackFunction, + type OIDCCallbackParams, + type OIDCResponse, + type Workflow } from '../mongodb_oidc'; import { finishCommandDocument, startCommandDocument } from './command_builders'; -import type { TokenCache, TokenEntry } from './token_cache'; - -/** The current version of OIDC implementation. */ -const OIDC_VERSION = 1; +import type { TokenCache } from './token_cache'; /** 5 minutes in milliseconds */ -const TIMEOUT_MS = 300000; +export const HUMAN_TIMEOUT_MS = 300000; +/** 1 minute in milliseconds */ +export const AUTOMATED_TIMEOUT_MS = 60000; /** Properties allowed on results of callbacks. */ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; @@ -43,26 +41,13 @@ export abstract class CallbackWorkflow implements Workflow { } /** - * 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. + * Each workflow should specify the correct custom behaviour for reauthentication. */ - async reauthenticate( + abstract reauthenticate( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache - ): Promise { - // Reauthentication should always remove the access token. - cache?.remove(); - return await this.execute(connection, credentials, cache); - } + cache: TokenCache + ): Promise; /** * Execute the OIDC callback workflow. @@ -70,30 +55,16 @@ export abstract class CallbackWorkflow implements Workflow { abstract execute( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache, + cache: TokenCache, response?: Document ): Promise; - /** - * Performs the one-step authorisation flow as defined in the OIDC auth spec. - */ - protected async oneStepAuth( - connection: Connection, - credentials: MongoCredentials, - callback: OIDCCallbackFunction, - cache?: TokenCache - ): Promise { - const tokenEntry = await this.fetchAccessToken(connection, credentials, callback); - cache?.put(tokenEntry); - return await this.finishAuthentication(connection, credentials, tokenEntry.idpServerResponse); - } - /** * Starts the callback authentication process. If there is a speculative * authentication document from the initial handshake, then we will use that * value to get the issuer, otherwise we will send the saslStart command. */ - private async startAuthentication( + protected async startAuthentication( connection: Connection, credentials: MongoCredentials, response?: Document @@ -117,34 +88,21 @@ export abstract class CallbackWorkflow implements Workflow { protected async finishAuthentication( connection: Connection, credentials: MongoCredentials, - tokenResult: IdPServerResponse, + token: string, conversationId?: number ): Promise { const result = await connection.command( ns(credentials.source), - finishCommandDocument(tokenResult.accessToken, conversationId), + finishCommandDocument(token, conversationId), undefined ); return result; } - /** - * Fetches an access token using either the request or refresh callbacks and - * puts it in the cache. - */ - protected async fetchAccessToken( - connection: Connection, - credentials: MongoCredentials, + protected async executeAndValidateCallback( callback: OIDCCallbackFunction, - idpInfo?: IdPInfo - ): Promise { - const params: OIDCCallbackParams = { - timeoutContext: AbortSignal.timeout(TIMEOUT_MS), - version: OIDC_VERSION - }; - if (idpInfo) { - params.idpInfo = idpInfo; - } + params: OIDCCallbackParams + ): Promise { // With no token in the cache we use the request callback. const result = await callback(params); // Validate that the result returned by the callback is acceptable. If it is not @@ -152,7 +110,7 @@ export abstract class CallbackWorkflow implements Workflow { if (isCallbackResultInvalid(result)) { throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } - return { idpServerResponse: result, idpInfo: idpInfo }; + return result; } } diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index c75f734892b..e1f54983d0d 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -1,11 +1,17 @@ -import { type Document } from 'bson'; +import { BSON, type Document } from 'bson'; import { MongoMissingCredentialsError } from '../../../error'; import { type Connection } from '../../connection'; import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; -import { type OIDCCallbackFunction } from '../mongodb_oidc'; -import { CallbackWorkflow } from './callback_workflow'; -import { type TokenCache, type TokenEntry } from './token_cache'; +import { + type IdPInfo, + OIDC_VERSION, + type OIDCCallbackFunction, + type OIDCCallbackParams, + type OIDCResponse +} from '../mongodb_oidc'; +import { CallbackWorkflow, HUMAN_TIMEOUT_MS } from './callback_workflow'; +import { type TokenCache } from './token_cache'; const NO_CALLBACK = 'No OIDC_HUMAN_CALLBACK provided for human callback workflow.'; @@ -15,38 +21,118 @@ const NO_CALLBACK = 'No OIDC_HUMAN_CALLBACK provided for human callback workflow */ export class HumanCallbackWorkflow extends CallbackWorkflow { /** - * Execute the OIDC callback workflow. + * 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, but in the + // human workflow we need to pass the refesh token through if it + // exists. + cache.removeAccessToken(); + return await this.execute(connection, credentials, cache); + } + + /** + * Execute the OIDC human callback workflow. */ async execute( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise { const callback = getCallback(credentials.mechanismProperties); - // If there is a cached access token, try to authenticate with it. If - // authentication fails with an Authentication error (18), - // invalidate the access token, fetch a new access token, and try - // to authenticate again. - // If the server fails for any other reason, do not clear the cache. - let tokenEntry: TokenEntry; - if (cache?.hasToken()) { - tokenEntry = cache.get(); + // Check if the Client Cache has an access token. + // If it does, cache the access token in the Connection Cache and perform a One-Step SASL conversation + // using the access token. If the server returns an Authentication error (18), + // invalidate the access token token from the Client Cache, clear the Connection Cache, + // and restart the authentication flow. Raise any other errors to the user. On success, exit the algorithm. + if (cache.hasAccessToken) { + const token = cache.getAccessToken(); try { - return await this.finishAuthentication( - connection, - credentials, - tokenEntry.idpServerResponse - ); + return await this.finishAuthentication(connection, credentials, token); } catch (error) { if (error.code === 18) { - cache?.remove(); - return await this.oneStepAuth(connection, credentials, callback, cache); + cache.removeAccessToken(); + return await this.execute(connection, credentials, cache); } else { throw error; } } } - return await this.oneStepAuth(connection, credentials, callback, cache); + // Check if the Client Cache has a refresh token. + // If it does, call the OIDC Human Callback with the cached refresh token and IdpInfo to get a + // new access token. Cache the new access token in the Client Cache and Connection Cache. + // Perform a One-Step SASL conversation using the new access token. If the the server returns + // an Authentication error (18), clear the refresh token, invalidate the access token from the + // Client Cache, clear the Connection Cache, and restart the authentication flow. Raise any other + // errors to the user. On success, exit the algorithm. + if (cache.hasRefreshToken) { + const refreshToken = cache.getRefreshToken(); + const result = await this.fetchAccessToken(callback, cache.getIdpInfo(), refreshToken); + cache.put(result); + try { + return await this.finishAuthentication(connection, credentials, result.accessToken); + } catch (error) { + if (error.code === 18) { + cache.removeRefreshToken(); + return await this.execute(connection, credentials, cache); + } else { + throw error; + } + } + } + + console.log('starting regular 2 step'); + // Start a new Two-Step SASL conversation. + // Run a PrincipalStepRequest to get the IdpInfo. + // Call the OIDC Human Callback with the new IdpInfo to get a new access token and optional refresh + // token. Drivers MUST NOT pass a cached refresh token to the callback when performing + // a new Two-Step conversation. Cache the new IdpInfo and refresh token in the Client Cache and the + // new access token in the Client Cache and Connection Cache. + // Attempt to authenticate using a JwtStepRequest with the new access token. Raise any errors to the user. + const startResponse = await this.startAuthentication(connection, credentials); + console.log(startResponse); + const conversationId = startResponse.conversationId; + const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo; + const callbackResponse = await this.fetchAccessToken(callback, idpInfo); + cache.put(callbackResponse, idpInfo); + return await this.finishAuthentication( + connection, + credentials, + callbackResponse.accessToken, + conversationId + ); + } + + /** + * Fetches an access token using the callback. + */ + private async fetchAccessToken( + callback: OIDCCallbackFunction, + idpInfo: IdPInfo, + refreshToken?: string + ): Promise { + const params: OIDCCallbackParams = { + timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), + version: OIDC_VERSION, + idpInfo: idpInfo + }; + if (refreshToken) { + params.refreshToken = refreshToken; + } + return await this.executeAndValidateCallback(callback, params); } } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index bd5310a397d..fee5823b2c3 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -27,7 +27,7 @@ export abstract class MachineWorkflow implements Workflow { async execute( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise { const token = await this.getTokenFromCacheOrEnv(credentials, cache); const command = finishCommandDocument(token); @@ -41,17 +41,17 @@ export abstract class MachineWorkflow implements Workflow { async reauthenticate( connection: Connection, credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise { // Reauthentication implies the token has expired. - cache?.remove(); + cache.removeAccessToken(); return await this.execute(connection, credentials, cache); } /** * Get the document to add for speculative authentication. */ - async speculativeAuth(credentials: MongoCredentials, cache?: TokenCache): Promise { + async speculativeAuth(credentials: MongoCredentials, cache: TokenCache): Promise { const token = await this.getTokenFromCacheOrEnv(credentials, cache); const document = finishCommandDocument(token); document.db = credentials.source; @@ -63,15 +63,13 @@ export abstract class MachineWorkflow implements Workflow { */ private async getTokenFromCacheOrEnv( credentials: MongoCredentials, - cache?: TokenCache + cache: TokenCache ): Promise { - if (cache?.hasToken()) { - return cache.get().idpServerResponse.accessToken; + if (cache.hasAccessToken) { + return cache.getAccessToken(); } else { const token = await this.getToken(credentials); - cache?.put({ - idpServerResponse: { accessToken: token.access_token, expiresInSeconds: token.expires_in } - }); + cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); return token.access_token; } } diff --git a/src/cmap/auth/mongodb_oidc/token_cache.ts b/src/cmap/auth/mongodb_oidc/token_cache.ts index 29120f70893..e2f7ad4954b 100644 --- a/src/cmap/auth/mongodb_oidc/token_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_cache.ts @@ -1,32 +1,62 @@ import { MongoDriverError } from '../../../error'; -import type { IdPInfo, IdPServerResponse } from '../mongodb_oidc'; +import type { IdPInfo, OIDCResponse } from '../mongodb_oidc'; -/** @internal */ -export interface TokenEntry { - idpServerResponse: IdPServerResponse; - idpInfo?: IdPInfo; -} +class MongoOIDCError extends MongoDriverError {} /** @internal */ export class TokenCache { - private entry?: TokenEntry; + private accessToken?: string; + private refreshToken?: string; + private idpInfo?: IdPInfo; + private expiresInSeconds?: number; + + get hasAccessToken(): boolean { + return !!this.accessToken; + } + + get hasRefreshToken(): boolean { + return !!this.refreshToken; + } + + get hasIdpInfo(): boolean { + return !!this.idpInfo; + } - hasToken(): boolean { - return !!this.entry; + getAccessToken(): string { + if (!this.accessToken) { + throw new MongoOIDCError('Attempted to get an access token when none exists.'); + } + return this.accessToken; + } + + getRefreshToken(): string { + if (!this.refreshToken) { + throw new MongoOIDCError('Attempted to get a refresh token when none exists.'); + } + return this.refreshToken; + } + + getIdpInfo(): IdPInfo { + if (!this.idpInfo) { + throw new MongoOIDCError('Attempted to get IDP information when none exists.'); + } + return this.idpInfo; } - get(): TokenEntry { - if (!this.entry) { - throw new MongoDriverError('Requested an OIDC token entry which is not in the cache.'); + put(response: OIDCResponse, idpInfo?: IdPInfo) { + this.accessToken = response.accessToken; + this.refreshToken = response.refreshToken; + this.expiresInSeconds = response.expiresInSeconds; + if (idpInfo) { + this.idpInfo = idpInfo; } - return this.entry; } - put(result: TokenEntry) { - this.entry = result; + removeAccessToken() { + this.accessToken = undefined; } - remove() { - this.entry = undefined; + removeRefreshToken() { + this.refreshToken = undefined; } } diff --git a/src/index.ts b/src/index.ts index 0fd66e5d934..3646180fa20 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,7 +100,7 @@ export { export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; -export { TokenCache, TokenEntry } from './cmap/auth/mongodb_oidc/token_cache'; +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'; diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts index a27c8fcafee..0979063c730 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -26,9 +26,6 @@ describe('OIDC Auth Spec Azure Tests', function () { // Close the client. beforeEach(function () { const options: MongoClientOptions = {}; - // if (process.env.AZUREOIDC_USERNAME) { - // options.auth = { username: process.env.AZUREOIDC_USERNAME, password: undefined }; - // } if (process.env.AZUREOIDC_RESOURCE) { options.authMechanismProperties = { TOKEN_RESOURCE: process.env.AZUREOIDC_RESOURCE }; } diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index 25821009560..2dd0e9f0703 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -26,7 +26,7 @@ const createCallback = (tokenFile = 'test_user1', expiresInSeconds?: number, ext // Generates the result the request or refresh callback returns. const generateResult = (token: string, expiresInSeconds?: number, extraFields?: any) => { - const response: OIDCResponse = { accessToken: token }; + const response: OIDCResponse = { accessToken: token, refreshToken: token }; if (expiresInSeconds) { response.expiresInSeconds = expiresInSeconds; } @@ -65,7 +65,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); }); @@ -86,7 +87,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); }); @@ -118,7 +120,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); }); @@ -139,7 +142,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); }); @@ -161,7 +165,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); }); @@ -182,7 +187,8 @@ describe('OIDC Auth Spec Tests', function () { authMechanismProperties: { OIDC_CALLBACK: callbackSpy, ENVIRONMENT: 'test' - } + }, + retryReads: false }); } catch (error) { expect(error).to.exist; @@ -210,7 +216,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); const provider = client.s.authProviders.getOrCreateProvider( 'MONGODB-OIDC' @@ -237,7 +244,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); const provider = client.s.authProviders.getOrCreateProvider( 'MONGODB-OIDC' @@ -278,7 +286,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); await client @@ -359,7 +368,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); await client @@ -414,7 +424,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); await client @@ -470,7 +481,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('test'); await collection.insertOne({ n: 1 }); @@ -526,7 +538,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -550,7 +563,8 @@ describe('OIDC Auth Spec Tests', function () { }, authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -574,7 +588,8 @@ describe('OIDC Auth Spec Tests', function () { }, authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -598,7 +613,8 @@ describe('OIDC Auth Spec Tests', function () { }, authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -618,7 +634,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriMulti, { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -640,7 +657,8 @@ describe('OIDC Auth Spec Tests', function () { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy, ALLOWED_HOSTS: [] - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -663,7 +681,8 @@ describe('OIDC Auth Spec Tests', function () { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy, ALLOWED_HOSTS: ['example.com'] - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -706,12 +725,13 @@ describe('OIDC Auth Spec Tests', function () { beforeEach(function () { client = new MongoClient(uriSingle, { auth: { - username: `test_machine${process.env.OIDC_DOMAIN}`, + username: `test_machine`, password: undefined }, authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -723,7 +743,7 @@ describe('OIDC Auth Spec Tests', function () { }); }); - describe('1. OIDC Human Callback Validation', function () { + describe('2. OIDC Human Callback Validation', function () { let client: MongoClient; let collection: Collection; @@ -740,7 +760,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -762,7 +783,8 @@ describe('OIDC Auth Spec Tests', function () { client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_HUMAN_CALLBACK: callbackSpy - } + }, + retryReads: false }); collection = client.db('test').collection('testHuman'); }); @@ -772,6 +794,301 @@ describe('OIDC Auth Spec Tests', function () { expect(error).to.exist; }); }); + + describe('2.3 Refresh Token Is Passed To The Callback', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create a MongoClient with a human callback that checks for the presence of a refresh token. + // Perform a find operation that succeeds. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 + // } + // } + // Perform a find operation that succeeds. + // Assert that the callback has been called twice. + // Assert that the refresh token was provided to the callback once. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await collection.findOne(); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + expect(callbackSpy.lastCall.firstArg.refreshToken).to.not.be.null; + }); + }); + }); + + describe('3. Speculative Authentication', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('3.1 Uses speculative authentication if there is a cached token', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with a human callback that returns a valid token. + // Set a fail point for find commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // closeConnection: true + // } + // } + // Perform a find operation that fails. + // Set a fail point for saslStart commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "saslStart" + // ], + // errorCode: 18 + // } + // } + // Perform a find operation that succeeds. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + closeConnection: true + } + }); + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['saslStart'], + errorCode: 18 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('successfully authenticates', async function () { + const result = await collection.findOne(); + expect(result).to.be.null; + }); + }); + + describe('3.2 Does not use speculative authentication if there is no cached token', function () { + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with a human callback that returns a valid token. + // Set a fail point for saslStart commands of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "saslStart" + // ], + // errorCode: 18 + // } + // } + // Perform a find operation that fails. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['saslStart'], + errorCode: 18 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + }); + }); + }); + + describe('4. Reauthentication', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('4.1 Succeeds', function () { + const callbackSpy = sinon.spy(createCallback()); + const commandStartedEvents = []; + const commandSucceededEvents = []; + const commandFailedEvents = []; + // Create an OIDC configured client and add 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 human callback has been called once. + // Clear the listener state if possible. + // Force a reauthenication using a fail point of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform another find operation that succeeds. + // Assert that the human 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. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + monitorCommands: true, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + client.on('commandStarted', event => { + if (event.commandName === 'find') commandStartedEvents.push(event.commandName); + }); + client.on('commandSucceeded', event => { + if (event.commandName === 'find') commandSucceededEvents.push(event.commandName); + }); + client.on('commandFailed', event => { + if (event.commandName === 'find') commandFailedEvents.push(event.commandName); + }); + await client + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await client.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + expect(commandStartedEvents).to.deep.equal(['find', 'find']); + expect(commandSucceededEvents).to.deep.equal(['find']); + expect(commandFailedEvents).to.deep.equal(['find']); + }); + }); }); }); }); From 9631718b160e6ba5398fefa50fb34e1bd2d5cc11 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 29 Apr 2024 19:08:58 +0200 Subject: [PATCH 04/64] test: migrate human callback tests part three --- src/cmap/auth/mongodb_oidc.ts | 64 +++++------------ .../automated_callback_workflow.ts | 72 ++++++++++--------- .../mongodb_oidc/azure_machine_workflow.ts | 8 +++ .../auth/mongodb_oidc/callback_workflow.ts | 48 +++++++++---- .../auth/mongodb_oidc/gcp_machine_workflow.ts | 8 +++ .../mongodb_oidc/human_callback_workflow.ts | 71 +++++++----------- .../auth/mongodb_oidc/machine_workflow.ts | 42 ++++++----- .../mongodb_oidc/token_machine_workflow.ts | 8 ++- src/cmap/connect.ts | 18 +++-- src/cmap/connection_pool.ts | 3 +- src/index.ts | 1 + src/mongo_client_auth_providers.ts | 49 +++++++++++-- .../auth/mongodb_oidc_test.prose.test.ts | 16 ++--- 13 files changed, 228 insertions(+), 180 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 61af1983b9d..227b4144dd4 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,15 +1,13 @@ import type { Document } from 'bson'; -import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; +import { MongoDriverError, MongoMissingCredentialsError } from '../../error'; import type { HandshakeDocument } from '../connect'; import type { Connection } from '../connection'; import { type AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; -import { AutomatedCallbackWorkflow } from './mongodb_oidc/automated_callback_workflow'; import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow'; import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow'; -import { HumanCallbackWorkflow } from './mongodb_oidc/human_callback_workflow'; -import type { TokenCache } from './mongodb_oidc/token_cache'; +import { TokenCache } from './mongodb_oidc/token_cache'; import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow'; /** Error when credentials are missing. */ @@ -65,8 +63,9 @@ export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise; /** * Each workflow should specify the correct custom behaviour for reauthentication. */ - reauthenticate( - connection: Connection, - credentials: MongoCredentials, - cache: TokenCache - ): Promise; + reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; /** * Get the document to add for speculative authentication. */ - speculativeAuth(credentials: MongoCredentials, cache: TokenCache): Promise; + speculativeAuth(credentials: MongoCredentials): Promise; } /** @internal */ -export const OIDC_WORKFLOWS: Map = new Map(); -OIDC_WORKFLOWS.set('automated_callback', new AutomatedCallbackWorkflow()); -OIDC_WORKFLOWS.set('human_callback', new HumanCallbackWorkflow()); -OIDC_WORKFLOWS.set('test', new TokenMachineWorkflow()); -OIDC_WORKFLOWS.set('azure', new AzureMachineWorkflow()); -OIDC_WORKFLOWS.set('gcp', new GCPMachineWorkflow()); +export const OIDC_WORKFLOWS: Map Workflow> = new Map(); +OIDC_WORKFLOWS.set('test', () => new TokenMachineWorkflow(new TokenCache())); +OIDC_WORKFLOWS.set('azure', () => new AzureMachineWorkflow(new TokenCache())); +OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache())); /** * OIDC auth provider. * @experimental */ export class MongoDBOIDC extends AuthProvider { - cache: TokenCache; + workflow: Workflow; /** * Instantiate the auth provider. */ - constructor(cache: TokenCache) { + constructor(workflow?: Workflow) { super(); - this.cache = cache; + if (!workflow) { + throw new MongoDriverError(''); + } + this.workflow = workflow; } /** @@ -123,11 +118,10 @@ export class MongoDBOIDC extends AuthProvider { override async auth(authContext: AuthContext): Promise { const { connection, reauthenticating, response } = authContext; const credentials = getCredentials(authContext); - const workflow = getWorkflow(credentials); if (reauthenticating) { - await workflow.reauthenticate(connection, credentials, this.cache); + await this.workflow.reauthenticate(connection, credentials); } else { - await workflow.execute(connection, credentials, this.cache, response); + await this.workflow.execute(connection, credentials, response); } } @@ -139,8 +133,7 @@ export class MongoDBOIDC extends AuthProvider { authContext: AuthContext ): Promise { const credentials = getCredentials(authContext); - const workflow = getWorkflow(credentials); - const result = await workflow.speculativeAuth(credentials, this.cache); + const result = await this.workflow.speculativeAuth(credentials); return { ...handshakeDoc, ...result }; } } @@ -155,22 +148,3 @@ function getCredentials(authContext: AuthContext): MongoCredentials { } return credentials; } - -/** - * Gets either a device workflow or callback workflow. - */ -function getWorkflow(credentials: MongoCredentials): Workflow { - let workflow; - if (credentials.mechanismProperties.OIDC_HUMAN_CALLBACK) { - workflow = OIDC_WORKFLOWS.get('human_callback'); - } else { - const providerName = credentials.mechanismProperties.ENVIRONMENT; - workflow = OIDC_WORKFLOWS.get(providerName || 'automated_callback'); - } - if (!workflow) { - throw new MongoInvalidArgumentError( - `Could not load workflow for provider ${credentials.mechanismProperties.ENVIRONMENT}` - ); - } - return workflow; -} diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index eeb7a5ecbdc..f47db0814be 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -1,8 +1,8 @@ import { type Document } from 'bson'; +import { setTimeout } from 'timers/promises'; -import { MongoMissingCredentialsError } from '../../../error'; import { type Connection } from '../../connection'; -import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; +import { type MongoCredentials } from '../mongo_credentials'; import { OIDC_VERSION, type OIDCCallbackFunction, @@ -12,13 +12,24 @@ import { import { AUTOMATED_TIMEOUT_MS, CallbackWorkflow } from './callback_workflow'; import { type TokenCache } from './token_cache'; -const NO_CALLBACK = 'No OIDC_CALLBACK provided for callback workflow.'; +/** Must wait at least 100ms between invokations */ +const CALLBACK_DELAY = 100; /** * Class implementing behaviour for the non human callback workflow. * @internal */ export class AutomatedCallbackWorkflow extends CallbackWorkflow { + private lastInvokationTime: number; + + /** + * Instantiate the human callback workflow. + */ + constructor(cache: TokenCache, callback: OIDCCallbackFunction) { + super(cache, callback); + this.lastInvokationTime = Date.now(); + } + /** * Reauthenticate the callback workflow. * For reauthentication: @@ -31,66 +42,59 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { * - 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 { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token. - cache.removeAccessToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeAccessToken(); + return await this.execute(connection, credentials); } /** * Execute the OIDC callback workflow. */ - async execute( - connection: Connection, - credentials: MongoCredentials, - cache: TokenCache - ): Promise { - const callback = getCallback(credentials.mechanismProperties); + async execute(connection: Connection, credentials: MongoCredentials): Promise { // If there is a cached access token, try to authenticate with it. If // authentication fails with an Authentication error (18), // invalidate the access token, fetch a new access token, and try // to authenticate again. // If the server fails for any other reason, do not clear the cache. - if (cache.hasAccessToken) { - const token = cache.getAccessToken(); + if (this.cache.hasAccessToken) { + const token = this.cache.getAccessToken(); try { return await this.finishAuthentication(connection, credentials, token); } catch (error) { if (error.code === 18) { - cache.removeAccessToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeAccessToken(); + return await this.execute(connection, credentials); } else { throw error; } } } - const response = await this.fetchAccessToken(callback); - cache.put(response); + let response: OIDCResponse; + const now = Date.now(); + // Ensure a delay between invokations to not overload the callback. + if (now - this.lastInvokationTime > CALLBACK_DELAY) { + response = await this.fetchAccessToken(); + } else { + const responses = await Promise.all([ + setTimeout(CALLBACK_DELAY - (now - this.lastInvokationTime)), + this.fetchAccessToken() + ]); + response = responses[1]; + } + this.lastInvokationTime = now; + this.cache.put(response); return await this.finishAuthentication(connection, credentials, response.accessToken); } /** * Fetches the access token using the callback. */ - protected async fetchAccessToken(callback: OIDCCallbackFunction): Promise { + protected async fetchAccessToken(): Promise { const params: OIDCCallbackParams = { timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), version: OIDC_VERSION }; - return await this.executeAndValidateCallback(callback, params); - } -} - -/** - * Returns the callback from the mechanism properties. - */ -export function getCallback(mechanismProperties: AuthMechanismProperties): OIDCCallbackFunction { - if (mechanismProperties.OIDC_CALLBACK) { - return mechanismProperties.OIDC_CALLBACK; + return await this.executeAndValidateCallback(params); } - throw new MongoMissingCredentialsError(NO_CALLBACK); } diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 96b86b2f8f8..3099a506773 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -2,6 +2,7 @@ import { MongoAzureError } from '../../../error'; import { request } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; +import { type TokenCache } from './token_cache'; /** Base URL for getting Azure tokens. */ const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?'; @@ -23,6 +24,13 @@ const TOKEN_RESOURCE_MISSING_ERROR = * @internal */ export class AzureMachineWorkflow extends MachineWorkflow { + /** + * Instantiate the machine workflow. + */ + constructor(cache: TokenCache) { + super(cache); + } + /** * Get the token from the environment. */ diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 05adc2707a2..75ea34692c8 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,6 +1,6 @@ import { type Document } from 'bson'; -import { MongoMissingCredentialsError } from '../../../error'; +import { MongoDriverError, MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; @@ -11,7 +11,7 @@ import { type Workflow } from '../mongodb_oidc'; import { finishCommandDocument, startCommandDocument } from './command_builders'; -import type { TokenCache } from './token_cache'; +import { type TokenCache } from './token_cache'; /** 5 minutes in milliseconds */ export const HUMAN_TIMEOUT_MS = 300000; @@ -30,6 +30,17 @@ const CALLBACK_RESULT_ERROR = * @internal */ export abstract class CallbackWorkflow implements Workflow { + cache: TokenCache; + callback: OIDCCallbackFunction; + + /** + * Instantiate the callback workflow. + */ + constructor(cache: TokenCache, callback: OIDCCallbackFunction) { + this.cache = cache; + this.callback = this.withLock(callback); + } + /** * Get the document to add for speculative authentication. This also needs * to add a db field from the credentials source. @@ -43,11 +54,7 @@ export abstract class CallbackWorkflow implements Workflow { /** * Each workflow should specify the correct custom behaviour for reauthentication. */ - abstract reauthenticate( - connection: Connection, - credentials: MongoCredentials, - cache: TokenCache - ): Promise; + abstract reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; /** * Execute the OIDC callback workflow. @@ -55,7 +62,6 @@ export abstract class CallbackWorkflow implements Workflow { abstract execute( connection: Connection, credentials: MongoCredentials, - cache: TokenCache, response?: Document ): Promise; @@ -99,12 +105,15 @@ export abstract class CallbackWorkflow implements Workflow { return result; } - protected async executeAndValidateCallback( - callback: OIDCCallbackFunction, - params: OIDCCallbackParams - ): Promise { + /** + * Executes the callback and validates the output. + */ + protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise { + if (!this.callback) { + throw new MongoDriverError(''); + } // With no token in the cache we use the request callback. - const result = await callback(params); + const result = await this.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)) { @@ -112,6 +121,19 @@ export abstract class CallbackWorkflow implements Workflow { } return result; } + + /** + * Ensure the callback is only executed one at a time. + */ + protected withLock(callback: OIDCCallbackFunction) { + let lock: Promise = Promise.resolve(); + return async (params: OIDCCallbackParams): Promise => { + await lock; + // eslint-disable-next-line github/no-then + lock = lock.then(() => callback(params)); + return await lock; + }; + } } /** diff --git a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts index c0b04c68e63..f021ec35bf9 100644 --- a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -2,6 +2,7 @@ import { MongoGCPError } from '../../../error'; import { request } from '../../../utils'; import { type MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; +import { type TokenCache } from './token_cache'; /** GCP base URL. */ const GCP_BASE_URL = @@ -15,6 +16,13 @@ const TOKEN_RESOURCE_MISSING_ERROR = 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is gcp.'; export class GCPMachineWorkflow extends MachineWorkflow { + /** + * Instantiate the machine workflow. + */ + constructor(cache: TokenCache) { + super(cache); + } + /** * Get the token from the environment. */ diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index e1f54983d0d..a5d6171f9b7 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -1,8 +1,7 @@ import { BSON, type Document } from 'bson'; -import { MongoMissingCredentialsError } from '../../../error'; import { type Connection } from '../../connection'; -import { type AuthMechanismProperties, type MongoCredentials } from '../mongo_credentials'; +import { type MongoCredentials } from '../mongo_credentials'; import { type IdPInfo, OIDC_VERSION, @@ -13,13 +12,18 @@ import { import { CallbackWorkflow, HUMAN_TIMEOUT_MS } from './callback_workflow'; import { type TokenCache } from './token_cache'; -const NO_CALLBACK = 'No OIDC_HUMAN_CALLBACK provided for human callback workflow.'; - /** * Class implementing behaviour for the non human callback workflow. * @internal */ export class HumanCallbackWorkflow extends CallbackWorkflow { + /** + * Instantiate the human callback workflow. + */ + constructor(cache: TokenCache, callback: OIDCCallbackFunction) { + super(cache, callback); + } + /** * Reauthenticate the callback workflow. * For reauthentication: @@ -32,40 +36,31 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { * - 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 { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token, but in the // human workflow we need to pass the refesh token through if it // exists. - cache.removeAccessToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeAccessToken(); + return await this.execute(connection, credentials); } /** * Execute the OIDC human callback workflow. */ - async execute( - connection: Connection, - credentials: MongoCredentials, - cache: TokenCache - ): Promise { - const callback = getCallback(credentials.mechanismProperties); + async execute(connection: Connection, credentials: MongoCredentials): Promise { // Check if the Client Cache has an access token. // If it does, cache the access token in the Connection Cache and perform a One-Step SASL conversation // using the access token. If the server returns an Authentication error (18), // invalidate the access token token from the Client Cache, clear the Connection Cache, // and restart the authentication flow. Raise any other errors to the user. On success, exit the algorithm. - if (cache.hasAccessToken) { - const token = cache.getAccessToken(); + if (this.cache.hasAccessToken) { + const token = this.cache.getAccessToken(); try { return await this.finishAuthentication(connection, credentials, token); } catch (error) { if (error.code === 18) { - cache.removeAccessToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeAccessToken(); + return await this.execute(connection, credentials); } else { throw error; } @@ -78,16 +73,16 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { // an Authentication error (18), clear the refresh token, invalidate the access token from the // Client Cache, clear the Connection Cache, and restart the authentication flow. Raise any other // errors to the user. On success, exit the algorithm. - if (cache.hasRefreshToken) { - const refreshToken = cache.getRefreshToken(); - const result = await this.fetchAccessToken(callback, cache.getIdpInfo(), refreshToken); - cache.put(result); + if (this.cache.hasRefreshToken) { + const refreshToken = this.cache.getRefreshToken(); + const result = await this.fetchAccessToken(this.cache.getIdpInfo(), refreshToken); + this.cache.put(result); try { return await this.finishAuthentication(connection, credentials, result.accessToken); } catch (error) { if (error.code === 18) { - cache.removeRefreshToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeRefreshToken(); + return await this.execute(connection, credentials); } else { throw error; } @@ -106,8 +101,8 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { console.log(startResponse); const conversationId = startResponse.conversationId; const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo; - const callbackResponse = await this.fetchAccessToken(callback, idpInfo); - cache.put(callbackResponse, idpInfo); + const callbackResponse = await this.fetchAccessToken(idpInfo); + this.cache.put(callbackResponse, idpInfo); return await this.finishAuthentication( connection, credentials, @@ -119,11 +114,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { /** * Fetches an access token using the callback. */ - private async fetchAccessToken( - callback: OIDCCallbackFunction, - idpInfo: IdPInfo, - refreshToken?: string - ): Promise { + private async fetchAccessToken(idpInfo: IdPInfo, refreshToken?: string): Promise { const params: OIDCCallbackParams = { timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), version: OIDC_VERSION, @@ -132,16 +123,6 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { if (refreshToken) { params.refreshToken = refreshToken; } - return await this.executeAndValidateCallback(callback, params); - } -} - -/** - * Returns a human callback from the mechanism properties. - */ -export function getCallback(mechanismProperties: AuthMechanismProperties): OIDCCallbackFunction { - if (mechanismProperties.OIDC_HUMAN_CALLBACK) { - return mechanismProperties.OIDC_HUMAN_CALLBACK; + return await this.executeAndValidateCallback(params); } - throw new MongoMissingCredentialsError(NO_CALLBACK); } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index fee5823b2c3..ab94a60cd70 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -21,15 +21,20 @@ export interface AccessToken { * @internal */ export abstract class MachineWorkflow implements Workflow { + cache: TokenCache; + + /** + * Instantiate the machine workflow. + */ + constructor(cache: TokenCache) { + this.cache = cache; + } + /** * 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); + async execute(connection: Connection, credentials: MongoCredentials): Promise { + const token = await this.getTokenFromCacheOrEnv(credentials); const command = finishCommandDocument(token); return await connection.command(ns(credentials.source), command, undefined); } @@ -38,21 +43,17 @@ export abstract class MachineWorkflow implements Workflow { * 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 { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication implies the token has expired. - cache.removeAccessToken(); - return await this.execute(connection, credentials, cache); + this.cache.removeAccessToken(); + return await this.execute(connection, credentials); } /** * Get the document to add for speculative authentication. */ - async speculativeAuth(credentials: MongoCredentials, cache: TokenCache): Promise { - const token = await this.getTokenFromCacheOrEnv(credentials, cache); + async speculativeAuth(credentials: MongoCredentials): Promise { + const token = await this.getTokenFromCacheOrEnv(credentials); const document = finishCommandDocument(token); document.db = credentials.source; return { speculativeAuthenticate: document }; @@ -61,15 +62,12 @@ export abstract class MachineWorkflow implements Workflow { /** * Get the token from the cache or environment. */ - private async getTokenFromCacheOrEnv( - credentials: MongoCredentials, - cache: TokenCache - ): Promise { - if (cache.hasAccessToken) { - return cache.getAccessToken(); + private async getTokenFromCacheOrEnv(credentials: MongoCredentials): Promise { + if (this.cache.hasAccessToken) { + return this.cache.getAccessToken(); } else { const token = await this.getToken(credentials); - cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); + this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); return token.access_token; } } diff --git a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts index ea6eb50b830..de32c469594 100644 --- a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import { MongoAWSError } from '../../../error'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; +import { type TokenCache } from './token_cache'; /** Error for when the token is missing in the environment. */ const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; @@ -12,8 +13,11 @@ const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; * @internal */ export class TokenMachineWorkflow extends MachineWorkflow { - constructor() { - super(); + /** + * Instantiate the machine workflow. + */ + constructor(cache: TokenCache) { + super(cache); } /** diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index abc530f8805..e319dbbed9b 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -91,7 +91,10 @@ export async function performInitialHandshake( if (credentials) { if ( !(credentials.mechanism === AuthMechanism.MONGODB_DEFAULT) && - !options.authProviders.getOrCreateProvider(credentials.mechanism) + !options.authProviders.getOrCreateProvider( + credentials.mechanism, + credentials.mechanismProperties + ) ) { throw new MongoInvalidArgumentError(`AuthMechanism '${credentials.mechanism}' not supported`); } @@ -146,7 +149,10 @@ export async function performInitialHandshake( authContext.response = response; const resolvedCredentials = credentials.resolveAuthMechanism(response); - const provider = options.authProviders.getOrCreateProvider(resolvedCredentials.mechanism); + const provider = options.authProviders.getOrCreateProvider( + resolvedCredentials.mechanism, + resolvedCredentials.mechanismProperties + ); if (!provider) { throw new MongoInvalidArgumentError( `No AuthProvider for ${resolvedCredentials.mechanism} defined.` @@ -218,7 +224,8 @@ export async function prepareHandshakeDocument( handshakeDoc.saslSupportedMechs = `${credentials.source}.${credentials.username}`; const provider = authContext.options.authProviders.getOrCreateProvider( - AuthMechanism.MONGODB_SCRAM_SHA256 + AuthMechanism.MONGODB_SCRAM_SHA256, + credentials.mechanismProperties ); if (!provider) { // This auth mechanism is always present. @@ -228,7 +235,10 @@ export async function prepareHandshakeDocument( } return await provider.prepare(handshakeDoc, authContext); } - const provider = authContext.options.authProviders.getOrCreateProvider(credentials.mechanism); + const provider = authContext.options.authProviders.getOrCreateProvider( + credentials.mechanism, + credentials.mechanismProperties + ); if (!provider) { throw new MongoInvalidArgumentError(`No AuthProvider for ${credentials.mechanism} defined.`); } diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 7c271e8a97f..f91e1361f65 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -551,7 +551,8 @@ export class ConnectionPool extends TypedEventEmitter { const resolvedCredentials = credentials.resolveAuthMechanism(connection.hello); const provider = this[kServer].topology.client.s.authProviders.getOrCreateProvider( - resolvedCredentials.mechanism + resolvedCredentials.mechanism, + resolvedCredentials.mechanismProperties ); if (!provider) { diff --git a/src/index.ts b/src/index.ts index 3646180fa20..712b49a1629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,6 +100,7 @@ export { export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; +export { Workflow } from './cmap/auth/mongodb_oidc'; export { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export { AuthMechanism } from './cmap/auth/providers'; export { Compressor } from './cmap/wire_protocol/compression'; diff --git a/src/mongo_client_auth_providers.ts b/src/mongo_client_auth_providers.ts index f3444faf162..7b2b66698dc 100644 --- a/src/mongo_client_auth_providers.ts +++ b/src/mongo_client_auth_providers.ts @@ -1,8 +1,11 @@ import { type AuthProvider } from './cmap/auth/auth_provider'; import { GSSAPI } from './cmap/auth/gssapi'; +import { type AuthMechanismProperties } from './cmap/auth/mongo_credentials'; import { MongoCR } from './cmap/auth/mongocr'; import { MongoDBAWS } from './cmap/auth/mongodb_aws'; -import { MongoDBOIDC } from './cmap/auth/mongodb_oidc'; +import { MongoDBOIDC, OIDC_WORKFLOWS, type Workflow } from './cmap/auth/mongodb_oidc'; +import { AutomatedCallbackWorkflow } from './cmap/auth/mongodb_oidc/automated_callback_workflow'; +import { HumanCallbackWorkflow } from './cmap/auth/mongodb_oidc/human_callback_workflow'; import { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; import { Plain } from './cmap/auth/plain'; import { AuthMechanism } from './cmap/auth/providers'; @@ -11,11 +14,11 @@ import { X509 } from './cmap/auth/x509'; import { MongoInvalidArgumentError } from './error'; /** @internal */ -const AUTH_PROVIDERS = new Map AuthProvider>([ +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(new TokenCache())], + [AuthMechanism.MONGODB_OIDC, (workflow?: Workflow) => new MongoDBOIDC(workflow)], [AuthMechanism.MONGODB_PLAIN, () => new Plain()], [AuthMechanism.MONGODB_SCRAM_SHA1, () => new ScramSHA1()], [AuthMechanism.MONGODB_SCRAM_SHA256, () => new ScramSHA256()], @@ -34,22 +37,56 @@ export class MongoClientAuthProviders { * Get or create an authentication provider based on the provided mechanism. * We don't want to create all providers at once, as some providers may not be used. * @param name - The name of the provider to get or create. + * @param credentials - The credentials. * @returns The provider. * @throws MongoInvalidArgumentError if the mechanism is not supported. * @internal */ - getOrCreateProvider(name: AuthMechanism | string): AuthProvider { + getOrCreateProvider( + name: AuthMechanism | string, + authMechanismProperties: AuthMechanismProperties + ): AuthProvider { const authProvider = this.existingProviders.get(name); if (authProvider) { return authProvider; } - const provider = AUTH_PROVIDERS.get(name)?.(); - if (!provider) { + const providerFunction = AUTH_PROVIDERS.get(name); + if (!providerFunction) { throw new MongoInvalidArgumentError(`authMechanism ${name} not supported`); } + let provider; + if (name === AuthMechanism.MONGODB_OIDC) { + provider = providerFunction(this.getWorkflow(authMechanismProperties)); + } else { + provider = providerFunction(); + } + this.existingProviders.set(name, provider); return provider; } + + /** + * Gets either a device workflow or callback workflow. + */ + getWorkflow(authMechanismProperties: AuthMechanismProperties): Workflow { + if (authMechanismProperties.OIDC_HUMAN_CALLBACK) { + return new HumanCallbackWorkflow( + new TokenCache(), + authMechanismProperties.OIDC_HUMAN_CALLBACK + ); + } else if (authMechanismProperties.OIDC_CALLBACK) { + return new AutomatedCallbackWorkflow(new TokenCache(), authMechanismProperties.OIDC_CALLBACK); + } else { + const environment = authMechanismProperties.ENVIRONMENT; + const workflow = OIDC_WORKFLOWS.get(environment)?.(); + if (!workflow) { + throw new MongoInvalidArgumentError( + `Could not load workflow for environment ${authMechanismProperties.ENVIRONMENT}` + ); + } + return workflow; + } + } } diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index 2dd0e9f0703..3657e0c61bb 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -219,10 +219,10 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); - const provider = client.s.authProviders.getOrCreateProvider( - 'MONGODB-OIDC' - ) as MongoDBOIDC; - provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC', { + OIDC_CALLBACK: callbackSpy + }) as MongoDBOIDC; + provider.workflow.cache.put({ idpServerResponse: { accessToken: 'bad' } }); collection = client.db('test').collection('test'); }); @@ -247,10 +247,10 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); - const provider = client.s.authProviders.getOrCreateProvider( - 'MONGODB-OIDC' - ) as MongoDBOIDC; - provider.cache.put({ idpServerResponse: { accessToken: 'bad' } }); + const provider = client.s.authProviders.getOrCreateProvider('MONGODB-OIDC', { + OIDC_CALLBACK: callbackSpy + }) as MongoDBOIDC; + provider.workflow.cache.put({ idpServerResponse: { accessToken: 'bad' } }); collection = client.db('test').collection('test'); }); From af1f424d209a1675c4c0c7f9441af8743c222eb8 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 29 Apr 2024 21:49:47 +0200 Subject: [PATCH 05/64] fix: callback speculative auth --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 12 +++++++++--- .../integration/auth/mongodb_oidc_test.prose.test.ts | 4 +++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 75ea34692c8..c2b5569ea62 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -46,9 +46,15 @@ export abstract class CallbackWorkflow implements Workflow { * to add a db field from the credentials source. */ async speculativeAuth(credentials: MongoCredentials): Promise { - const document = startCommandDocument(credentials); - document.db = credentials.source; - return { speculativeAuthenticate: document }; + // Check if the Client Cache has an access token. + // If it does, cache the access token in the Connection Cache and send a JwtStepRequest + // with the cached access token in the speculative authentication SASL payload. + if (this.cache.hasAccessToken) { + const document = finishCommandDocument(this.cache.getAccessToken()); + document.db = credentials.source; + return { speculativeAuthenticate: document }; + } + return {}; } /** diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index 3657e0c61bb..0345190cdb1 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -971,13 +971,14 @@ describe('OIDC Auth Spec Tests', function () { retryReads: false }); collection = client.db('test').collection('testHuman'); + console.log('setting fail point'); await client .db() .admin() .command({ configureFailPoint: 'failCommand', mode: { - times: 1 + times: 2 }, data: { failCommands: ['saslStart'], @@ -994,6 +995,7 @@ describe('OIDC Auth Spec Tests', function () { }); it('does not successfully authenticate', async function () { + console.log('execute find'); const error = await collection.findOne().catch(error => error); expect(error).to.exist; }); From 57454256bdd7dbf76a1c1d087d068aa2abfded9e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 1 May 2024 22:15:46 +0200 Subject: [PATCH 06/64] test: use util clients in prose tests --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 2 +- .../mongodb_oidc/human_callback_workflow.ts | 2 - .../auth/mongodb_oidc_test.prose.test.ts | 100 ++++++++++++++---- 4 files changed, 84 insertions(+), 22 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index f420d1dedef..7db2987fccb 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1256,7 +1256,7 @@ tasks: ENVIRONMENT: aws SCRIPT: run-oidc-unified-tests.sh args: - - .evergreen/run-oidc-tests-aws.sh + - .evergreen/run-oidc-tests-test.sh - name: "oidc-auth-test-gcp-latest" commands: diff --git a/.evergreen/config.yml b/.evergreen/config.yml index cf548c55127..507868f5384 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1209,7 +1209,7 @@ tasks: ENVIRONMENT: aws SCRIPT: run-oidc-unified-tests.sh args: - - .evergreen/run-oidc-tests-aws.sh + - .evergreen/run-oidc-tests-test.sh - name: oidc-auth-test-gcp-latest commands: - func: install dependencies diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index a5d6171f9b7..f3a0c847de4 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -89,7 +89,6 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { } } - console.log('starting regular 2 step'); // Start a new Two-Step SASL conversation. // Run a PrincipalStepRequest to get the IdpInfo. // Call the OIDC Human Callback with the new IdpInfo to get a new access token and optional refresh @@ -98,7 +97,6 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { // new access token in the Client Cache and Connection Cache. // Attempt to authenticate using a JwtStepRequest with the new access token. Raise any errors to the user. const startResponse = await this.startAuthentication(connection, credentials); - console.log(startResponse); const conversationId = startResponse.conversationId; const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo; const callbackResponse = await this.fetchAccessToken(idpInfo); diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index 0345190cdb1..f6a2181a089 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { expect } from 'chai'; +import { expect, util } from 'chai'; import * as sinon from 'sinon'; import { @@ -262,6 +262,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('3.3 Unexpected error code does not clear the cache', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); // Create a MongoClient with a callback that returns a valid token. // Set a fail point for saslStart commands of the form: @@ -289,8 +290,14 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('test'); - await client + await utilClient .db() .admin() .command({ @@ -306,10 +313,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('successfully authenticates the second time', async function () { @@ -346,6 +354,7 @@ describe('OIDC Auth Spec Tests', function () { }; describe('4.1 Reauthentication Succeeds', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); // Create an OIDC configured client. // Set a fail point for find commands of the form: @@ -371,8 +380,14 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('test'); - await client + await utilClient .db() .admin() .command({ @@ -388,10 +403,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('successfully authenticates', async function () { @@ -401,6 +417,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('4.2 Read Commands Fail If Reauthentication Fails', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createBadCallback()); // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. // Perform a find operation that succeeds. @@ -427,8 +444,14 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('test'); - await client + await utilClient .db() .admin() .command({ @@ -444,10 +467,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('does not successfully authenticate', async function () { @@ -458,6 +482,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('4.3 Write Commands Fail If Reauthentication Fails', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createBadCallback()); // Create a MongoClient whose OIDC callback returns one good token and then bad tokens after the first call. // Perform an insert operation that succeeds. @@ -484,9 +509,15 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('test'); await collection.insertOne({ n: 1 }); - await client + await utilClient .db() .admin() .command({ @@ -502,10 +533,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('does not successfully authenticate', async function () { @@ -796,6 +828,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('2.3 Refresh Token Is Passed To The Callback', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); // Create a MongoClient with a human callback that checks for the presence of a refresh token. // Perform a find operation that succeeds. @@ -822,9 +855,15 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('testHuman'); await collection.findOne(); - await client + await utilClient .db() .admin() .command({ @@ -840,10 +879,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('successfully authenticates', async function () { @@ -863,6 +903,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('3.1 Uses speculative authentication if there is a cached token', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); // Create an OIDC configured client with a human callback that returns a valid token. // Set a fail point for find commands of the form: @@ -901,8 +942,14 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('testHuman'); - await client + await utilClient .db() .admin() .command({ @@ -917,7 +964,7 @@ describe('OIDC Auth Spec Tests', function () { }); const error = await collection.findOne().catch(error => error); expect(error).to.exist; - await client + await utilClient .db() .admin() .command({ @@ -933,10 +980,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('successfully authenticates', async function () { @@ -946,6 +994,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('3.2 Does not use speculative authentication if there is no cached token', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); // Create an OIDC configured client with a human callback that returns a valid token. // Set a fail point for saslStart commands of the form: @@ -970,9 +1019,15 @@ describe('OIDC Auth Spec Tests', function () { }, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('testHuman'); console.log('setting fail point'); - await client + await utilClient .db() .admin() .command({ @@ -988,10 +1043,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('does not successfully authenticate', async function () { @@ -1011,6 +1067,7 @@ describe('OIDC Auth Spec Tests', function () { }); describe('4.1 Succeeds', function () { + let utilClient: MongoClient; const callbackSpy = sinon.spy(createCallback()); const commandStartedEvents = []; const commandSucceededEvents = []; @@ -1049,6 +1106,12 @@ describe('OIDC Auth Spec Tests', function () { monitorCommands: true, retryReads: false }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); collection = client.db('test').collection('testHuman'); await collection.findOne(); expect(callbackSpy).to.have.been.calledOnce; @@ -1061,7 +1124,7 @@ describe('OIDC Auth Spec Tests', function () { client.on('commandFailed', event => { if (event.commandName === 'find') commandFailedEvents.push(event.commandName); }); - await client + await utilClient .db() .admin() .command({ @@ -1077,10 +1140,11 @@ describe('OIDC Auth Spec Tests', function () { }); afterEach(async function () { - await client.db().admin().command({ + await utilClient.db().admin().command({ configureFailPoint: 'failCommand', mode: 'off' }); + await utilClient.close(); }); it('successfully authenticates', async function () { From 107180f307f3e893b788a02f5f33003f90e0223c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 16:50:55 +0200 Subject: [PATCH 07/64] fix: addressing comments --- .evergreen/run-oidc-prose-tests.sh | 2 - package.json | 1 - src/cmap/auth/mongo_credentials.ts | 6 - src/cmap/auth/mongodb_oidc.ts | 16 +- .../auth/mongodb_oidc/command_builders.ts | 6 +- .../mongodb_oidc/human_callback_workflow.ts | 11 +- src/cmap/auth/providers.ts | 1 - src/error.ts | 1 + .../auth/mongodb_oidc_azure.prose.test.ts | 2 +- test/manual/mongodb_oidc.prose.test.ts | 836 ------------------ .../azure_machine_workflow.test.ts | 9 +- .../mongodb_oidc/gcp_machine_workflow.test.ts | 4 +- .../token_machine_workflow.test.ts | 9 +- 13 files changed, 39 insertions(+), 865 deletions(-) delete mode 100644 test/manual/mongodb_oidc.prose.test.ts diff --git a/.evergreen/run-oidc-prose-tests.sh b/.evergreen/run-oidc-prose-tests.sh index ee0e1afc568..ae9de15d361 100755 --- a/.evergreen/run-oidc-prose-tests.sh +++ b/.evergreen/run-oidc-prose-tests.sh @@ -6,8 +6,6 @@ ENVIRONMENT=${ENVIRONMENT:-"test"} PROJECT_DIRECTORY=${PROJECT_DIRECTORY:-"."} source "${PROJECT_DIRECTORY}/.evergreen/init-node-and-npm-env.sh" -printenv - if [ -z "${MONGODB_URI_SINGLE}" ]; then echo "Must specify MONGODB_URI_SINGLE" exit 1 diff --git a/package.json b/package.json index 4ab34acc6e5..f859cd51ae5 100644 --- a/package.json +++ b/package.json @@ -148,7 +148,6 @@ "check:drivers-atlas-testing": "mocha --config test/mocha_mongodb.json test/atlas/drivers_atlas_testing.test.ts", "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-test": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_test.prose.test.ts", "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.test.ts", diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 0b05a89f437..f6e2ecfbcde 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -61,17 +61,11 @@ export interface AuthMechanismProperties extends Document { SERVICE_REALM?: string; CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue; AWS_SESSION_TOKEN?: string; - /** @experimental */ OIDC_CALLBACK?: OIDCCallbackFunction; - /** @experimental */ OIDC_HUMAN_CALLBACK?: OIDCCallbackFunction; - /** @experimental */ ENVIRONMENT?: 'test' | 'azure' | 'gcp'; - /** @experimental */ ALLOWED_HOSTS?: string[]; - /** @experimental */ TOKEN_RESOURCE?: string; - /** @experimental */ TOKEN_CLIENT_ID?: string; } diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 227b4144dd4..d3ac9c0b8b3 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -14,8 +14,8 @@ import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow'; const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; /** + * The information returned by the server on the IDP server. * @public - * @experimental */ export interface IdPInfo { issuer: string; @@ -24,8 +24,9 @@ export interface IdPInfo { } /** + * The response from the IdP server with the access token and + * optional expiration time and refresh token. * @public - * @experimental */ export interface IdPServerResponse { accessToken: string; @@ -34,8 +35,9 @@ export interface IdPServerResponse { } /** + * The response required to be returned from the machine or + * human callback workflows' callback. * @public - * @experimental */ export interface OIDCResponse { accessToken: string; @@ -44,19 +46,20 @@ export interface OIDCResponse { } /** + * The parameters that the driver provides to the user supplied + * human or machine callback. * @public - * @experimental */ export interface OIDCCallbackParams { timeoutContext: AbortSignal; - version: number; + version: 1; idpInfo?: IdPInfo; refreshToken?: string; } /** + * The signature of the human or machine callback functions. * @public - * @experimental */ export type OIDCCallbackFunction = (params: OIDCCallbackParams) => Promise; @@ -96,7 +99,6 @@ OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache())); /** * OIDC auth provider. - * @experimental */ export class MongoDBOIDC extends AuthProvider { workflow: Workflow; diff --git a/src/cmap/auth/mongodb_oidc/command_builders.ts b/src/cmap/auth/mongodb_oidc/command_builders.ts index ee6284343f3..6795e75a65a 100644 --- a/src/cmap/auth/mongodb_oidc/command_builders.ts +++ b/src/cmap/auth/mongodb_oidc/command_builders.ts @@ -7,8 +7,8 @@ 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') { +export function finishCommandDocument(token: string, conversationId?: number) { + if (conversationId != null) { return { saslContinue: 1, conversationId: conversationId, @@ -29,7 +29,7 @@ export function finishCommandDocument(token: string, conversationId?: number): D /** * Generate the saslStart command document. */ -export function startCommandDocument(credentials: MongoCredentials): Document { +export function startCommandDocument(credentials: MongoCredentials) { const payload: Document = {}; if (credentials.username) { payload.n = credentials.username; diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index f3a0c847de4..edb4651f4c0 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -11,6 +11,7 @@ import { } from '../mongodb_oidc'; import { CallbackWorkflow, HUMAN_TIMEOUT_MS } from './callback_workflow'; import { type TokenCache } from './token_cache'; +import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; /** * Class implementing behaviour for the non human callback workflow. @@ -58,7 +59,10 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { try { return await this.finishAuthentication(connection, credentials, token); } catch (error) { - if (error.code === 18) { + if ( + error instanceof MongoError && + error.code === MONGODB_ERROR_CODES.AuthenticationFailed + ) { this.cache.removeAccessToken(); return await this.execute(connection, credentials); } else { @@ -80,7 +84,10 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { try { return await this.finishAuthentication(connection, credentials, result.accessToken); } catch (error) { - if (error.code === 18) { + if ( + error instanceof MongoError && + error.code === MONGODB_ERROR_CODES.AuthenticationFailed + ) { this.cache.removeRefreshToken(); return await this.execute(connection, credentials); } else { diff --git a/src/cmap/auth/providers.ts b/src/cmap/auth/providers.ts index d01c06324bb..74e3638ecc5 100644 --- a/src/cmap/auth/providers.ts +++ b/src/cmap/auth/providers.ts @@ -8,7 +8,6 @@ export const AuthMechanism = Object.freeze({ MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1', MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256', MONGODB_X509: 'MONGODB-X509', - /** @experimental */ MONGODB_OIDC: 'MONGODB-OIDC' } as const); diff --git a/src/error.ts b/src/error.ts index 5c607abfdb9..024147d1815 100644 --- a/src/error.ts +++ b/src/error.ts @@ -36,6 +36,7 @@ export const NODE_IS_RECOVERING_ERROR_MESSAGE = new RegExp('node is recovering', export const MONGODB_ERROR_CODES = Object.freeze({ HostUnreachable: 6, HostNotFound: 7, + AuthenticationFailed: 18, NetworkTimeout: 89, ShutdownInProgress: 91, PrimarySteppedDown: 189, diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts index 0979063c730..847678537e4 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -57,7 +57,7 @@ describe('OIDC Auth Spec Azure Tests', function () { it('does not authenticate', async function () { const error = await collection.findOne().catch(error => error); - expect(error.message).to.include(/Azure endpoint/); + expect(error.message).to.include('Azure endpoint'); }); }); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts deleted file mode 100644 index 2acac7c2a40..00000000000 --- a/test/manual/mongodb_oidc.prose.test.ts +++ /dev/null @@ -1,836 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import * as path from 'node:path'; - -import { expect } from 'chai'; -import * as sinon from 'sinon'; - -import { - type Collection, - type CommandFailedEvent, - type CommandStartedEvent, - type CommandSucceededEvent, - MongoClient, - MongoInvalidArgumentError, - MongoMissingCredentialsError, - MongoServerError, - type OIDCCallbackParams, - type OIDCResponse -} from '../mongodb'; - -describe('OIDC Auth Spec Prose Tests', function () { - context('when running in the environment', function () { - it('contains AWS_WEB_IDENTITY_TOKEN_FILE', function () { - expect(process.env).to.have.property('AWS_WEB_IDENTITY_TOKEN_FILE'); - }); - }); - - describe('1. Callback Authentication', function () { - // Creates a request function for use in the test. - const createRequestCallback = ( - username = 'test_user1', - expiresInSeconds?: number, - extraFields?: any - ) => { - return async (params: OIDCCallbackParams) => { - const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, username), { - encoding: 'utf8' - }); - // Do some basic property assertions. - 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: OIDCResponse = { accessToken: token }; - if (expiresInSeconds) { - response.expiresInSeconds = expiresInSeconds; - } - if (extraFields) { - return { ...response, ...extraFields }; - } - return response; - }; - - describe('1. Callback-Driven Auth', function () { - let client: MongoClient; - let collection: Collection; - - afterEach(async function () { - await client?.close(); - }); - - describe('1.1 Single Principal Implicit Username', function () { - before(function () { - // Create the default OIDC client. - client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { - authMechanismProperties: { - OIDC_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('1.2 Single Principal Explicit Username', function () { - before(function () { - // 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: { - OIDC_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('1.3 Multiple Principal User 1', function () { - before(function () { - // 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_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('1.4 Multiple Principal User 2', function () { - before(function () { - // 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_CALLBACK: createRequestCallback('test_user2') - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('1.5 Multiple Principal No User', function () { - before(function () { - // 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_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Assert that a find operation fails. - // Close the client. - it('fails authentication', async function () { - try { - await collection.findOne(); - expect.fail('Expected OIDC auth to fail with no user provided'); - } catch (e) { - expect(e).to.be.instanceOf(MongoServerError); - expect(e.message).to.include('Authentication failed'); - } - }); - }); - - describe('1.6 Allowed Hosts Blocked', function () { - // 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: [], - OIDC_CALLBACK: createRequestCallback() - } - }); - 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); - expect(error.message).to.include( - 'is not valid for OIDC authentication with ALLOWED_HOSTS' - ); - }); - }); - - // Create a client that uses the url ``mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com`` a request callback, and an - // ``ALLOWED_HOSTS`` that contains ["example.com"]. - // Assert that a ``find`` operation fails with a client-side error. - // Close the client. - context('when ALLOWED_HOSTS does not match', function () { - beforeEach(function () { - this.currentTest.skipReason = 'Will fail URI parsing as ignored is not a valid option'; - this.skip(); - // client = new MongoClient( - // 'mongodb://localhost/?authMechanism=MONGODB-OIDC&ignored=example.com', - // { - // authMechanismProperties: { - // ALLOWED_HOSTS: ['example.com'], - // OIDC_CALLBACK: createRequestCallback('test_user1', 600) - // } - // } - // ); - // collection = client.db('test').collection('test'); - }); - - it('fails validation', async function () { - // try { - // await collection.findOne(); - // } catch (error) { - // expect(error).to.be.instanceOf(MongoInvalidArgumentError); - // expect(error.message).to.include('Host does not match provided ALLOWED_HOSTS values'); - // } - }); - }); - - // Create a client that uses the url ``mongodb://evilmongodb.com`` a request - // callback, and an ``ALLOWED_HOSTS`` that contains ``*mongodb.com``. - // Assert that a ``find`` operation fails with a client-side error. - // Close the client. - context('when ALLOWED_HOSTS is invalid', function () { - before(function () { - client = new MongoClient('mongodb://evilmongodb.com/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - ALLOWED_HOSTS: ['*mongodb.com'], - OIDC_CALLBACK: createRequestCallback('test_user1', 600) - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - it('fails validation', async function () { - const error = await collection.findOne().catch(error => error); - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error.message).to.include( - 'is not valid for OIDC authentication with ALLOWED_HOSTS' - ); - }); - }); - }); - }); - - describe('2. AWS Automatic Auth', function () { - let client: MongoClient; - let collection: Collection; - - afterEach(async function () { - await client?.close(); - }); - - describe('2.1 Single Principal', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws' - ); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws. - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('2.2 Multiple Principal User 1', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred. - // Perform a find operation that succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('2.3 Multiple Principal User 2', function () { - let tokenFile; - - before(function () { - tokenFile = process.env.AWS_WEB_IDENTITY_TOKEN_FILE; - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = path.join( - process.env.OIDC_TOKEN_DIR, - 'test_user2' - ); - client = new MongoClient( - 'mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('nodeOidcTest'); - }); - - after(function () { - process.env.AWS_WEB_IDENTITY_TOKEN_FILE = tokenFile; - }); - - // Set the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws&directConnection=true&readPreference=secondaryPreferred. - // Perform a find operation that succeeds. - // Close the client. - // Restore the AWS_WEB_IDENTITY_TOKEN_FILE environment variable to the location of valid test_user2 credentials. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - describe('2.4 Allowed Hosts Ignored', function () { - before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', - { - authMechanismProperties: { - ALLOWED_HOSTS: [] - } - } - ); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws, and an ALLOWED_HOSTS that is an empty list. - // Assert that a find operation succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - }); - - describe('3. Callback Validation', function () { - let client: MongoClient; - let collection: Collection; - - afterEach(async function () { - await client?.close(); - }); - - 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 authMechanismProperties = { - OIDC_CALLBACK: requestSpy - }; - - before(async function () { - // 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('nodeOidcTest'); - }); - - // 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 () { - await collection.findOne(); - expect(requestSpy).to.have.been.calledOnce; - }); - }); - - describe('3.2 Request Callback Returns Null', function () { - before(function () { - // Create a client with a request callback that returns null. - client = new MongoClient(`${process.env.MONGODB_URI_SINGLE}?authMechanism=MONGODB-OIDC`, { - authMechanismProperties: { - OIDC_CALLBACK: () => { - return Promise.resolve(null); - } - } - }); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // 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 null return 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.4 Request Callback Returns Invalid Data', function () { - context('when the request callback has missing fields', function () { - before(function () { - // 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_CALLBACK: () => { - return Promise.resolve({}); - } - } - } - ); - collection = client.db('test').collection('nodeOidcTest'); - }); - - // 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 invlid return 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('4. Speculative Authentication', function () { - let client: MongoClient; - const requestCallback = createRequestCallback('test_user1', 600); - const authMechanismProperties = { - OIDC_CALLBACK: requestCallback - }; - - // Removes the fail point. - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; - - // Sets up the fail point for the saslStart - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 2 - }, - data: { - failCommands: ['saslStart'], - errorCode: 18 - } - }); - }; - - afterEach(async function () { - await removeFailPoint(); - await client?.close(); - }); - - before(async function () { - // 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(); - }); - - // Perform a find operation that succeeds. - // Close the client. - it('successfully speculative authenticates', async function () { - const result = await client.db('test').collection('nodeOidcTest').findOne(); - expect(result).to.be.null; - }); - }); - - describe('5. Reauthentication', function () { - let client: MongoClient; - - // Removes the fail point. - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; - - describe('5.1 Succeeds', function () { - const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); - const authMechanismProperties = { - OIDC_CALLBACK: requestSpy - }; - const commandStartedEvents: CommandStartedEvent[] = []; - const commandSucceededEvents: CommandSucceededEvent[] = []; - const commandFailedEvents: CommandFailedEvent[] = []; - - const commandStartedListener = event => { - if (event.commandName === 'find') { - commandStartedEvents.push(event); - } - }; - const commandSucceededListener = event => { - if (event.commandName === 'find') { - commandSucceededEvents.push(event); - } - }; - const commandFailedListener = event => { - if (event.commandName === 'find') { - commandFailedEvents.push(event); - } - }; - - const addListeners = () => { - client.on('commandStarted', commandStartedListener); - client.on('commandSucceeded', commandSucceededListener); - client.on('commandFailed', commandFailedListener); - }; - - // Sets up the fail point for the find to reauthenticate. - const setupFailPoint = async () => { - return await client - .db() - .admin() - .command({ - configureFailPoint: 'failCommand', - mode: { - times: 1 - }, - data: { - failCommands: ['find'], - errorCode: 391 - } - }); - }; - - before(async function () { - // 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 - }); - // 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 () { - await removeFailPoint(); - await client.close(); - }); - - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 1 - // }, - // "data": { - // "failCommands": [ - // "find" - // ], - // "errorCode": 391 - // } - // } - // - // Note - // - // 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 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 () { - await setupFailPoint(); - 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' - ]); - expect(commandSucceededEvents.map(event => event.commandName)).to.deep.equal(['find']); - expect(commandFailedEvents.map(event => event.commandName)).to.deep.equal(['find']); - }); - }); - - describe('5.2 Succeeds no refresh', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const requestSpy = sinon.spy(requestCallback); - const authMechanismProperties = { - OIDC_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: 1 - }, - data: { - failCommands: ['find'], - errorCode: 391 - } - }); - }; - - before(async function () { - // 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 - }); - // 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: - // - // { - // "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_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: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "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('nodeOidcTest').findOne(); - expect(requestSpy).to.have.been.calledThrice; - expect(result).to.be.null; - }); - }); - - describe('5.3 Fails', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const requestSpy = sinon.spy(requestCallback); - const authMechanismProperties = { - OIDC_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', 'saslStart'], - 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 (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(); - }); - - afterEach(async function () { - await removeFailPoint(); - await client.close(); - }); - - // Force a reauthenication using a failCommand of the form: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // 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('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/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts index 68bcb71bc90..bb265671d20 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -1,11 +1,16 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { AzureMachineWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; +import { + AzureMachineWorkflow, + Connection, + MongoCredentials, + TokenCache +} from '../../../../mongodb'; describe('AzureMachineFlow', function () { describe('#execute', function () { - const workflow = new AzureMachineWorkflow(); + const workflow = new AzureMachineWorkflow(new TokenCache()); context('when TOKEN_RESOURCE is not set', function () { const connection = sinon.createStubInstance(Connection); 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 index 73ac6c7869f..caafbf1ecf4 100644 --- a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Connection, GCPMachineWorkflow, MongoCredentials } from '../../../../mongodb'; +import { Connection, GCPMachineWorkflow, MongoCredentials, TokenCache } from '../../../../mongodb'; describe('GCPMachineFlow', function () { describe('#execute', function () { - const workflow = new GCPMachineWorkflow(); + const workflow = new GCPMachineWorkflow(new TokenCache()); context('when TOKEN_RESOURCE is not set', function () { const connection = sinon.createStubInstance(Connection); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts index 33ea4960ca2..b75e6380d57 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -1,11 +1,16 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Connection, MongoCredentials, TokenMachineWorkflow } from '../../../../mongodb'; +import { + Connection, + MongoCredentials, + TokenCache, + TokenMachineWorkflow +} from '../../../../mongodb'; describe('TokenMachineFlow', function () { describe('#execute', function () { - const workflow = new TokenMachineWorkflow(); + const workflow = new TokenMachineWorkflow(new TokenCache()); context('when OIDC_TOKEN_FILE is not in the env', function () { let file; From eba741db669abe353f39d6ca1356fcfc6b588ccf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 19:05:42 +0200 Subject: [PATCH 08/64] fix: next comment addressing --- src/index.ts | 2 +- test/spec/auth/legacy/connection-string.json | 153 ++++++- test/spec/auth/legacy/connection-string.yml | 116 ++++- .../auth/unified/mongodb-oidc-no-retry.json | 421 ++++++++++++++++++ .../auth/unified/mongodb-oidc-no-retry.yml | 228 ++++++++++ .../auth/unified/oidc-auth-with-retry.json | 170 ------- .../auth/unified/oidc-auth-with-retry.yml | 92 ---- .../auth/unified/oidc-auth-without-retry.json | 175 -------- .../auth/unified/oidc-auth-without-retry.yml | 94 ---- .../azure_machine_workflow.test.ts | 8 +- .../mongodb_oidc/gcp_machine_workflow.test.ts | 8 +- .../token_machine_workflow.test.ts | 12 +- test/unit/index.test.ts | 1 - 13 files changed, 905 insertions(+), 575 deletions(-) create mode 100644 test/spec/auth/unified/mongodb-oidc-no-retry.json create mode 100644 test/spec/auth/unified/mongodb-oidc-no-retry.yml delete mode 100644 test/spec/auth/unified/oidc-auth-with-retry.json delete mode 100644 test/spec/auth/unified/oidc-auth-with-retry.yml delete mode 100644 test/spec/auth/unified/oidc-auth-without-retry.json delete mode 100644 test/spec/auth/unified/oidc-auth-without-retry.yml diff --git a/src/index.ts b/src/index.ts index 712b49a1629..c5422f68b55 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,7 @@ export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; export { Workflow } from './cmap/auth/mongodb_oidc'; -export { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; +export type { 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'; diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index 42b13ff9774..f4c7f8c88ea 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -482,8 +482,8 @@ } }, { - "description": "should recognise the mechanism with aws provider (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws", + "description": "should recognise the mechanism with test environment (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": true, "credential": { "username": null, @@ -491,13 +491,13 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "ENVIRONMENT": "aws" + "ENVIRONMENT": "test" } } }, { - "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=ENVIRONMENT:aws", + "description": "should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test", "valid": true, "credential": { "username": null, @@ -505,30 +505,30 @@ "source": "$external", "mechanism": "MONGODB-OIDC", "mechanism_properties": { - "ENVIRONMENT": "aws" + "ENVIRONMENT": "test" } } }, { "description": "should throw an exception if supplied a password (MONGODB-OIDC)", - "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if username is specified for aws (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:aws", + "description": "should throw an exception if username is specified for test (MONGODB-OIDC)", + "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test", "valid": false, "credential": null }, { - "description": "should throw an exception if specified provider is not supported (MONGODB-OIDC)", + "description": "should throw an exception if specified environment is not supported (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid", "valid": false, "credential": null }, { - "description": "should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC)", + "description": "should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC)", "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", "valid": false, "credential": null @@ -538,6 +538,135 @@ "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted", "valid": false, "credential": null + }, + { + "description": "should recognise the mechanism with azure provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a username with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "mongodb://test-cluster" + } + } + }, + { + "description": "should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "mongodb://test-cluster" + } + } + }, + { + "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abc%2Cd%25ef%3Ag%26hi", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "abc,d%ef:g&hi" + } + } + }, + { + "description": "should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b", + "valid": true, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "a$b" + } + } + }, + { + "description": "should accept a username and throw an error for a password with azure provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an exception if no token audience is given for azure provider (MONGODB-OIDC)", + "uri": "mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure", + "valid": false, + "credential": null + }, + { + "description": "should recognise the mechanism with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": true, + "credential": { + "username": null, + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "gcp", + "TOKEN_RESOURCE": "foo" + } + } + }, + { + "description": "should throw an error for a username and password with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo", + "valid": false, + "credential": null + }, + { + "description": "should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC)", + "uri": "mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp", + "valid": false, + "credential": null } ] -} +} \ No newline at end of file diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index 7e11c5678c8..c88eb1edce8 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -350,8 +350,8 @@ tests: mechanism: MONGODB-AWS mechanism_properties: AWS_SESSION_TOKEN: token!@#$%^&*()_+ -- description: should recognise the mechanism with aws provider (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws +- description: should recognise the mechanism with test environment (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test valid: true credential: username: @@ -359,9 +359,9 @@ tests: source: "$external" mechanism: MONGODB-OIDC mechanism_properties: - ENVIRONMENT: aws -- 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=ENVIRONMENT:aws + ENVIRONMENT: test +- description: should recognise the mechanism when auth source is explicitly specified and with environment (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external&authMechanismProperties=ENVIRONMENT:test valid: true credential: username: @@ -369,20 +369,20 @@ tests: source: "$external" mechanism: MONGODB-OIDC mechanism_properties: - ENVIRONMENT: aws + ENVIRONMENT: test - description: should throw an exception if supplied a password (MONGODB-OIDC) - uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test valid: false credential: -- description: should throw an exception if username is specified for aws (MONGODB-OIDC) - uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:aws +- description: should throw an exception if username is specified for test (MONGODB-OIDC) + uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC&ENVIRONMENT:test valid: false credential: -- description: should throw an exception if specified provider is not supported (MONGODB-OIDC) +- description: should throw an exception if specified environment is not supported (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:invalid valid: false credential: -- description: should throw an exception if neither provider nor callbacks specified (MONGODB-OIDC) +- description: should throw an exception if neither environment nor callbacks specified (MONGODB-OIDC) uri: mongodb://localhost/?authMechanism=MONGODB-OIDC valid: false credential: @@ -390,3 +390,97 @@ tests: uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted valid: false credential: +- description: should recognise the mechanism with azure provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: true + credential: + username: null + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: foo +- description: should accept a username with azure provider (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: foo +- description: should accept a url-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb%3A%2F%2Ftest-cluster + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'mongodb://test-cluster' +- description: should accept an un-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:mongodb://test-cluster + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'mongodb://test-cluster' +- description: should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abc%2Cd%25ef%3Ag%26hi + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'abc,d%ef:g&hi' +- description: should url-encode a TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: a$b +- description: should accept a username and throw an error for a password with azure provider (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:foo + valid: false + credential: null +- description: should throw an exception if no token audience is given for azure provider (MONGODB-OIDC) + uri: mongodb://username@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure + valid: false + credential: null +- description: should recognise the mechanism with gcp provider (MONGODB-OIDC) + uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo + valid: true + credential: + username: null + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: gcp + TOKEN_RESOURCE: foo +- description: should throw an error for a username and password with gcp provider + (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp,TOKEN_RESOURCE:foo + valid: false + credential: null +- description: should throw an error if not TOKEN_RESOURCE with gcp provider (MONGODB-OIDC) + uri: mongodb://user:pass@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:gcp + valid: false + credential: null diff --git a/test/spec/auth/unified/mongodb-oidc-no-retry.json b/test/spec/auth/unified/mongodb-oidc-no-retry.json new file mode 100644 index 00000000000..9dbe1982704 --- /dev/null +++ b/test/spec/auth/unified/mongodb-oidc-no-retry.json @@ -0,0 +1,421 @@ +{ + "description": "MONGODB-OIDC authentication with retry disabled", + "schemaVersion": "1.19", + "runOnRequirements": [ + { + "minServerVersion": "7.0", + "auth": true, + "authMechanism": "MONGODB-OIDC" + } + ], + "createEntities": [ + { + "client": { + "id": "failPointClient", + "useMultipleMongoses": false + } + }, + { + "client": { + "id": "client0", + "uriOptions": { + "authMechanism": "MONGODB-OIDC", + "authMechanismProperties": { + "$$placeholder": 1 + }, + "retryReads": false, + "retryWrites": false + }, + "observeEvents": [ + "commandStartedEvent", + "commandSucceededEvent", + "commandFailedEvent" + ] + } + }, + { + "database": { + "id": "database0", + "client": "client0", + "databaseName": "test" + } + }, + { + "collection": { + "id": "collection0", + "database": "database0", + "collectionName": "collName" + } + } + ], + "initialData": [ + { + "collectionName": "collName", + "databaseName": "test", + "documents": [] + } + ], + "tests": [ + { + "description": "A read operation should succeed", + "operations": [ + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "A write operation should succeed", + "operations": [ + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Read commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "find" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "find", + "object": "collection0", + "arguments": { + "filter": {} + }, + "expectResult": [] + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandFailedEvent": { + "commandName": "find" + } + }, + { + "commandStartedEvent": { + "command": { + "find": "collName", + "filter": {} + } + } + }, + { + "commandSucceededEvent": { + "commandName": "find" + } + } + ] + } + ] + }, + { + "description": "Write commands should reauthenticate and retry when a ReauthenticationRequired error happens", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "errorCode": 391 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake with cached token should use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "insert" + ], + "closeConnection": true + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "isClientError": true + } + }, + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 18 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + } + } + ], + "expectEvents": [ + { + "client": "client0", + "events": [ + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandFailedEvent": { + "commandName": "insert" + } + }, + { + "commandStartedEvent": { + "command": { + "insert": "collName", + "documents": [ + { + "_id": 1, + "x": 1 + } + ] + } + } + }, + { + "commandSucceededEvent": { + "commandName": "insert" + } + } + ] + } + ] + }, + { + "description": "Handshake without cached token should not use speculative authentication", + "operations": [ + { + "name": "failPoint", + "object": "testRunner", + "arguments": { + "client": "failPointClient", + "failPoint": { + "configureFailPoint": "failCommand", + "mode": { + "times": 1 + }, + "data": { + "failCommands": [ + "saslStart" + ], + "errorCode": 18 + } + } + } + }, + { + "name": "insertOne", + "object": "collection0", + "arguments": { + "document": { + "_id": 1, + "x": 1 + } + }, + "expectError": { + "errorCode": 18 + } + } + ] + } + ] +} diff --git a/test/spec/auth/unified/mongodb-oidc-no-retry.yml b/test/spec/auth/unified/mongodb-oidc-no-retry.yml new file mode 100644 index 00000000000..426fd72466c --- /dev/null +++ b/test/spec/auth/unified/mongodb-oidc-no-retry.yml @@ -0,0 +1,228 @@ +--- +description: "MONGODB-OIDC authentication with retry disabled" +schemaVersion: "1.19" +runOnRequirements: +- minServerVersion: "7.0" + auth: true + authMechanism: "MONGODB-OIDC" +createEntities: +- client: + id: &failPointClient failPointClient + useMultipleMongoses: false +- 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 EC2, replace the $$placeholder + # document with {"ENVIRONMENT": "test"}. + authMechanismProperties: { $$placeholder: 1 } + retryReads: false + retryWrites: false + observeEvents: + - commandStartedEvent + - commandSucceededEvent + - commandFailedEvent +- database: + id: database0 + client: client0 + databaseName: test +- collection: + id: collection0 + database: database0 + collectionName: collName +initialData: +- collectionName: collName + databaseName: test + documents: [] +tests: +- description: A read operation should succeed + operations: + - name: find + object: collection0 + arguments: + filter: {} + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: A write operation should succeed + operations: + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Read commands should reauthenticate and retry when a ReauthenticationRequired error happens + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - find + errorCode: 391 # ReauthenticationRequired + - name: find + object: collection0 + arguments: + filter: {} + expectResult: [] + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + find: collName + filter: {} + - commandFailedEvent: + commandName: find + - commandStartedEvent: + command: + find: collName + filter: {} + - commandSucceededEvent: + commandName: find +- description: Write commands should reauthenticate and retry when a ReauthenticationRequired error happens + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + errorCode: 391 # ReauthenticationRequired + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Handshake with cached token should use speculative authentication + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - insert + closeConnection: true + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectError: + isClientError: true + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - saslStart + errorCode: 18 + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectEvents: + - client: client0 + events: + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandFailedEvent: + commandName: insert + - commandStartedEvent: + command: + insert: collName + documents: + - _id: 1 + x: 1 + - commandSucceededEvent: + commandName: insert +- description: Handshake without cached token should not use speculative authentication + operations: + - name: failPoint + object: testRunner + arguments: + client: failPointClient + failPoint: + configureFailPoint: failCommand + mode: + times: 1 + data: + failCommands: + - saslStart + errorCode: 18 + - name: insertOne + object: collection0 + arguments: + document: + _id: 1 + x: 1 + expectError: + errorCode: 18 \ No newline at end of file diff --git a/test/spec/auth/unified/oidc-auth-with-retry.json b/test/spec/auth/unified/oidc-auth-with-retry.json deleted file mode 100644 index aeae3288c98..00000000000 --- a/test/spec/auth/unified/oidc-auth-with-retry.json +++ /dev/null @@ -1,170 +0,0 @@ -{ - "description": "OIDC authentication with retry", - "schemaVersion": "1.18", - "runOnRequirements": [ - { - "minServerVersion": "7.0", - "auth": true, - "authMechanism": "MONGODB-OIDC" - } - ], - "createEntities": [ - { - "client": { - "id": "client0", - "uriOptions": { - "authMechanism": "MONGODB-OIDC", - "authMechanismProperties": { - "$$placeholder": 1 - }, - "retryReads": true, - "retryWrites": true - }, - "observeEvents": [ - "commandStartedEvent", - "commandSucceededEvent", - "commandFailedEvent" - ] - } - }, - { - "database": { - "id": "database0", - "client": "client0", - "databaseName": "test" - } - }, - { - "collection": { - "id": "collection0", - "database": "database0", - "collectionName": "collName" - } - } - ], - "initialData": [ - { - "collectionName": "collName", - "databaseName": "test", - "documents": [ - - ] - } - ], - "tests": [ - { - "description": "A simple find operation should succeed", - "operations": [ - { - "name": "find", - "arguments": { - "filter": { - } - }, - "object": "collection0", - "expectResult": [ - - ] - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "find": "collName", - "filter": { - } - } - } - }, - { - "commandSucceededEvent": { - "commandName": "find" - } - } - ] - } - ] - }, - { - "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true", - "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "insert" - ], - "errorCode": 391 - } - } - } - }, - { - "name": "insertOne", - "object": "collection0", - "arguments": { - "document": { - "_id": 1, - "x": 1 - } - } - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "collName", - "documents": [ - { - "_id": 1, - "x": 1 - } - ] - } - } - }, - { - "commandFailedEvent": { - "commandName": "insert" - } - }, - { - "commandStartedEvent": { - "command": { - "insert": "collName", - "documents": [ - { - "_id": 1, - "x": 1 - } - ] - } - } - }, - { - "commandSucceededEvent": { - "commandName": "insert" - } - } - ] - } - ] - } - ] -} diff --git a/test/spec/auth/unified/oidc-auth-with-retry.yml b/test/spec/auth/unified/oidc-auth-with-retry.yml deleted file mode 100644 index e7a71b255e9..00000000000 --- a/test/spec/auth/unified/oidc-auth-with-retry.yml +++ /dev/null @@ -1,92 +0,0 @@ ---- -description: "OIDC authentication with retry" -schemaVersion: "1.18" -runOnRequirements: -- 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 {"ENVIRONMENT": "aws"}. - authMechanismProperties: { $$placeholder: 1 } - retryReads: true - retryWrites: true - observeEvents: - - commandStartedEvent - - commandSucceededEvent - - commandFailedEvent -- database: - id: database0 - client: client0 - databaseName: test -- collection: - id: collection0 - database: database0 - collectionName: collName -initialData: -- collectionName: collName - databaseName: test - documents: [] -tests: -- description: A simple find operation should succeed - operations: - - name: find - arguments: - filter: {} - object: collection0 - expectResult: [] - expectEvents: - - client: client0 - events: - - commandStartedEvent: - command: - find: collName - filter: {} - - commandSucceededEvent: - commandName: find -- description: Write command should reauthenticate when receive ReauthenticationRequired - error code and retryWrites=true - operations: - - name: failPoint - object: testRunner - arguments: - client: client0 - failPoint: - configureFailPoint: failCommand - mode: - times: 1 - data: - failCommands: - - insert - errorCode: 391 - - name: insertOne - object: collection0 - arguments: - document: - _id: 1 - x: 1 - expectEvents: - - client: client0 - events: - - commandStartedEvent: - command: - insert: collName - documents: - - _id: 1 - x: 1 - - commandFailedEvent: - commandName: insert - - commandStartedEvent: - command: - insert: collName - documents: - - _id: 1 - x: 1 - - commandSucceededEvent: - commandName: insert diff --git a/test/spec/auth/unified/oidc-auth-without-retry.json b/test/spec/auth/unified/oidc-auth-without-retry.json deleted file mode 100644 index ad8c93c03ff..00000000000 --- a/test/spec/auth/unified/oidc-auth-without-retry.json +++ /dev/null @@ -1,175 +0,0 @@ -{ - "description": "OIDC authentication without retry", - "schemaVersion": "1.18", - "runOnRequirements": [ - { - "minServerVersion": "7.0", - "auth": true, - "authMechanism": "MONGODB-OIDC" - } - ], - "createEntities": [ - { - "client": { - "id": "authClient" - } - }, - { - "client": { - "id": "client0", - "uriOptions": { - "authMechanism": "MONGODB-OIDC", - "authMechanismProperties": { - "$$placeholder": 1 - }, - "retryReads": true, - "retryWrites": true - }, - "observeEvents": [ - "commandStartedEvent", - "commandSucceededEvent", - "commandFailedEvent" - ] - } - }, - { - "database": { - "id": "database0", - "client": "client0", - "databaseName": "test" - } - }, - { - "collection": { - "id": "collection0", - "database": "database0", - "collectionName": "collName" - } - } - ], - "initialData": [ - { - "collectionName": "collName", - "databaseName": "test", - "documents": [ - - ] - } - ], - "tests": [ - { - "description": "A simple find operation should succeed", - "operations": [ - { - "name": "find", - "arguments": { - "filter": { - } - }, - "object": "collection0", - "expectResult": [ - - ] - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "find": "collName", - "filter": { - } - } - } - }, - { - "commandSucceededEvent": { - "commandName": "find" - } - } - ] - } - ] - }, - { - "description": "Write command should reauthenticate when receive ReauthenticationRequired error code and retryWrites=true", - "operations": [ - { - "name": "failPoint", - "object": "testRunner", - "arguments": { - "client": "client0", - "failPoint": { - "configureFailPoint": "failCommand", - "mode": { - "times": 1 - }, - "data": { - "failCommands": [ - "insert" - ], - "errorCode": 391 - } - } - } - }, - { - "name": "insertOne", - "object": "collection0", - "arguments": { - "document": { - "_id": 1, - "x": 1 - } - } - } - ], - "expectEvents": [ - { - "client": "client0", - "events": [ - { - "commandStartedEvent": { - "command": { - "insert": "collName", - "documents": [ - { - "_id": 1, - "x": 1 - } - ] - } - } - }, - { - "commandFailedEvent": { - "commandName": "insert" - } - }, - { - "commandStartedEvent": { - "command": { - "insert": "collName", - "documents": [ - { - "_id": 1, - "x": 1 - } - ] - } - } - }, - { - "commandSucceededEvent": { - "commandName": "insert" - } - } - ] - } - ] - } - ] -} diff --git a/test/spec/auth/unified/oidc-auth-without-retry.yml b/test/spec/auth/unified/oidc-auth-without-retry.yml deleted file mode 100644 index 2b8e33c13ca..00000000000 --- a/test/spec/auth/unified/oidc-auth-without-retry.yml +++ /dev/null @@ -1,94 +0,0 @@ ---- -description: "OIDC authentication without retry" -schemaVersion: "1.18" -runOnRequirements: -- minServerVersion: "7.0" - auth: true - authMechanism: "MONGODB-OIDC" -createEntities: -- client: - id: authClient -- 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 {"ENVIRONMENT": "aws"}. - authMechanismProperties: { $$placeholder: 1 } - retryReads: true - retryWrites: true - observeEvents: - - commandStartedEvent - - commandSucceededEvent - - commandFailedEvent -- database: - id: database0 - client: client0 - databaseName: test -- collection: - id: collection0 - database: database0 - collectionName: collName -initialData: -- collectionName: collName - databaseName: test - documents: [] -tests: -- description: A simple find operation should succeed - operations: - - name: find - arguments: - filter: {} - object: collection0 - expectResult: [] - expectEvents: - - client: client0 - events: - - commandStartedEvent: - command: - find: collName - filter: {} - - commandSucceededEvent: - commandName: find -- description: Write command should reauthenticate when receive ReauthenticationRequired - error code and retryWrites=true - operations: - - name: failPoint - object: testRunner - arguments: - client: client0 - failPoint: - configureFailPoint: failCommand - mode: - times: 1 - data: - failCommands: - - insert - errorCode: 391 - - name: insertOne - object: collection0 - arguments: - document: - _id: 1 - x: 1 - expectEvents: - - client: client0 - events: - - commandStartedEvent: - command: - insert: collName - documents: - - _id: 1 - x: 1 - - commandFailedEvent: - commandName: insert - - commandStartedEvent: - command: - insert: collName - documents: - - _id: 1 - x: 1 - - commandSucceededEvent: - commandName: insert 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 index bb265671d20..fddc1181d82 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -17,12 +17,8 @@ describe('AzureMachineFlow', function () { const credentials = sinon.createStubInstance(MongoCredentials); it('throws an error', async function () { - try { - await workflow.execute(connection, credentials); - expect.fail('workflow must fail without TOKEN_RESOURCE'); - } catch (error) { - expect(error.message).to.include('TOKEN_RESOURCE'); - } + const error = await workflow.execute(connection, credentials).catch(error => error); + expect(error.message).to.include('TOKEN_RESOURCE'); }); }); }); 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 index caafbf1ecf4..b401f96152e 100644 --- a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -12,12 +12,8 @@ describe('GCPMachineFlow', function () { const credentials = sinon.createStubInstance(MongoCredentials); it('throws an error', async function () { - try { - await workflow.execute(connection, credentials); - expect.fail('workflow must fail without TOKEN_RESOURCE'); - } catch (error) { - expect(error.message).to.include('TOKEN_RESOURCE'); - } + const error = await workflow.execute(connection, credentials).catch(error => error); + expect(error.message).to.include('TOKEN_RESOURCE'); }); }); }); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts index b75e6380d57..3507213dbfc 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -23,16 +23,14 @@ describe('TokenMachineFlow', function () { }); after(function () { - process.env.OIDC_TOKEN_FILE = file; + if (file) { + process.env.OIDC_TOKEN_FILE = file; + } }); it('throws an error', async function () { - try { - await workflow.execute(connection, credentials); - expect.fail('workflow must fail without OIDC_TOKEN_FILE'); - } catch (error) { - expect(error.message).to.include('OIDC_TOKEN_FILE'); - } + const error = await workflow.execute(connection, credentials).catch(error => error); + expect(error.message).to.include('OIDC_TOKEN_FILE'); }); }); }); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 726219ca1ad..edc27e44d57 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -125,7 +125,6 @@ const EXPECTED_EXPORTS = [ 'ServerType', 'SrvPollingEvent', 'Timestamp', - 'TokenCache', 'TopologyClosedEvent', 'TopologyDescriptionChangedEvent', 'TopologyOpeningEvent', From c94456446a7a643b9fa474061918402a62da5890 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 19:32:15 +0200 Subject: [PATCH 09/64] fix: next comment addressing --- .../mongodb_oidc/automated_callback_workflow.ts | 13 +++---------- .../auth/mongodb_oidc/human_callback_workflow.ts | 15 ++++----------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index f47db0814be..9408f63570a 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -31,16 +31,9 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } /** - * 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. + * Reauthenticate the callback workflow. For this we invalidated the access token + * in the cache and run the authentication steps again. No initial handshake needs + * to be sent. */ async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token. diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index edb4651f4c0..d09a3dd50e7 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -1,5 +1,6 @@ import { BSON, type Document } from 'bson'; +import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; import { type Connection } from '../../connection'; import { type MongoCredentials } from '../mongo_credentials'; import { @@ -11,7 +12,6 @@ import { } from '../mongodb_oidc'; import { CallbackWorkflow, HUMAN_TIMEOUT_MS } from './callback_workflow'; import { type TokenCache } from './token_cache'; -import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; /** * Class implementing behaviour for the non human callback workflow. @@ -26,16 +26,9 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { } /** - * 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. + * Reauthenticate the callback workflow. For this we invalidated the access token + * in the cache and run the authentication steps again. No initial handshake needs + * to be sent. */ async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token, but in the From dbe8d7f675e46c0aa4b0e1de66e5b1e7aa958f3a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 20:16:37 +0200 Subject: [PATCH 10/64] fix: next comment addressing tests and lint --- src/cmap/auth/mongodb_oidc.ts | 4 ++-- .../mongodb_oidc/automated_callback_workflow.ts | 14 +++++++------- src/cmap/auth/mongodb_oidc/command_builders.ts | 15 +++++++++++++-- .../auth/mongodb_oidc_test.prose.test.ts | 2 +- 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index d3ac9c0b8b3..f7f56f60535 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -1,6 +1,6 @@ import type { Document } from 'bson'; -import { MongoDriverError, MongoMissingCredentialsError } from '../../error'; +import { MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; import type { HandshakeDocument } from '../connect'; import type { Connection } from '../connection'; import { type AuthContext, AuthProvider } from './auth_provider'; @@ -109,7 +109,7 @@ export class MongoDBOIDC extends AuthProvider { constructor(workflow?: Workflow) { super(); if (!workflow) { - throw new MongoDriverError(''); + throw new MongoInvalidArgumentError('No workflow provided to the OIDC auth provider.'); } this.workflow = workflow; } diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 9408f63570a..7893e471c14 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -12,22 +12,22 @@ import { import { AUTOMATED_TIMEOUT_MS, CallbackWorkflow } from './callback_workflow'; import { type TokenCache } from './token_cache'; -/** Must wait at least 100ms between invokations */ -const CALLBACK_DELAY = 100; +/** Must wait at least 100ms between invocations */ +const CALLBACK_DEBOUNCE_MS = 100; /** * Class implementing behaviour for the non human callback workflow. * @internal */ export class AutomatedCallbackWorkflow extends CallbackWorkflow { - private lastInvokationTime: number; + private lastInvocationTime: number; /** * Instantiate the human callback workflow. */ constructor(cache: TokenCache, callback: OIDCCallbackFunction) { super(cache, callback); - this.lastInvokationTime = Date.now(); + this.lastInvocationTime = Date.now(); } /** @@ -66,16 +66,16 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { let response: OIDCResponse; const now = Date.now(); // Ensure a delay between invokations to not overload the callback. - if (now - this.lastInvokationTime > CALLBACK_DELAY) { + if (now - this.lastInvocationTime > CALLBACK_DEBOUNCE_MS) { response = await this.fetchAccessToken(); } else { const responses = await Promise.all([ - setTimeout(CALLBACK_DELAY - (now - this.lastInvokationTime)), + setTimeout(CALLBACK_DEBOUNCE_MS - (now - this.lastInvocationTime)), this.fetchAccessToken() ]); response = responses[1]; } - this.lastInvokationTime = now; + this.lastInvocationTime = now; this.cache.put(response); return await this.finishAuthentication(connection, credentials, response.accessToken); } diff --git a/src/cmap/auth/mongodb_oidc/command_builders.ts b/src/cmap/auth/mongodb_oidc/command_builders.ts index 6795e75a65a..2c2256e4afc 100644 --- a/src/cmap/auth/mongodb_oidc/command_builders.ts +++ b/src/cmap/auth/mongodb_oidc/command_builders.ts @@ -3,11 +3,22 @@ import { Binary, BSON, type Document } from 'bson'; import { type MongoCredentials } from '../mongo_credentials'; import { AuthMechanism } from '../providers'; +/** @internal */ +export interface OIDCCommand { + saslStart?: number; + saslContinue?: number; + conversationId?: number; + mechanism?: string; + autoAuthorize?: number; + db?: string; + payload: Binary; +} + /** * 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) { +export function finishCommandDocument(token: string, conversationId?: number): OIDCCommand { if (conversationId != null) { return { saslContinue: 1, @@ -29,7 +40,7 @@ export function finishCommandDocument(token: string, conversationId?: number) { /** * Generate the saslStart command document. */ -export function startCommandDocument(credentials: MongoCredentials) { +export function startCommandDocument(credentials: MongoCredentials): OIDCCommand { const payload: Document = {}; if (credentials.username) { payload.n = credentials.username; diff --git a/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc_test.prose.test.ts index f6a2181a089..a788f6b9e30 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_test.prose.test.ts @@ -1,7 +1,7 @@ import { readFile } from 'node:fs/promises'; import * as path from 'node:path'; -import { expect, util } from 'chai'; +import { expect } from 'chai'; import * as sinon from 'sinon'; import { From d329f78f2e9472457e27a7c4d3ba4b09990aef9a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 20:59:26 +0200 Subject: [PATCH 11/64] test: update command types --- .evergreen/config.in.yml | 6 ++++++ .evergreen/config.yml | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 7db2987fccb..2bc694e3e29 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1210,6 +1210,7 @@ tasks: commands: - func: "install dependencies" - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1221,6 +1222,7 @@ tasks: args: - .evergreen/run-oidc-tests-azure.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1236,6 +1238,7 @@ tasks: commands: - func: "install dependencies" - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1247,6 +1250,7 @@ tasks: args: - .evergreen/run-oidc-tests-test.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1262,6 +1266,7 @@ tasks: commands: - func: "install dependencies" - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1273,6 +1278,7 @@ tasks: args: - .evergreen/run-oidc-tests-gcp.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 507868f5384..3c45cbd21ee 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1164,6 +1164,7 @@ tasks: commands: - func: install dependencies - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1175,6 +1176,7 @@ tasks: args: - .evergreen/run-oidc-tests-azure.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1189,6 +1191,7 @@ tasks: commands: - func: install dependencies - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1200,6 +1203,7 @@ tasks: args: - .evergreen/run-oidc-tests-test.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1214,6 +1218,7 @@ tasks: commands: - func: install dependencies - command: subprocess.exec + type: test params: working_dir: src binary: bash @@ -1225,6 +1230,7 @@ tasks: args: - .evergreen/run-oidc-tests-gcp.sh - command: subprocess.exec + type: test params: working_dir: src binary: bash From 80332e8f56626386c6269fd301783686a94e0b4f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 21:59:07 +0200 Subject: [PATCH 12/64] fix: use test in azure endpoint --- src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 3099a506773..a2bc4a2d60a 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,5 +1,5 @@ +import { get } from '../../../client-side-encryption/providers/utils'; import { MongoAzureError } from '../../../error'; -import { request } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; import { type TokenCache } from './token_cache'; @@ -58,11 +58,15 @@ async function getAzureTokenData(tokenAudience: string, username?: string): Prom if (username) { url.searchParams.append('client_id', username); } - const data = await request(url.toString(), { - json: true, + const response = await get(url.toString(), { headers: AZURE_HEADERS }); - return data as AccessToken; + if (response.status !== 200) { + throw new MongoAzureError( + `Status code ${response.status} returned from the Azure endpoint. Response body ${response.body}` + ); + } + return JSON.parse(response.body); } /** From 86d6c49b019693ae117530bcce00bdb8400c60c2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 2 May 2024 22:05:56 +0200 Subject: [PATCH 13/64] test: more comments --- .evergreen/config.in.yml | 2 +- .evergreen/config.yml | 2 +- src/cmap/auth/mongodb_oidc.ts | 3 +++ .../automated_callback_workflow.ts | 2 +- .../mongodb_oidc/azure_machine_workflow.ts | 2 +- .../auth/mongodb_oidc/machine_workflow.ts | 4 ++++ test/tools/unified-spec-runner/entities.ts | 6 ------ test/tools/unified-spec-runner/runner.ts | 2 -- .../unified-spec-runner/unified-utils.ts | 19 ++----------------- 9 files changed, 13 insertions(+), 29 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 2bc694e3e29..3d1c5fbd79c 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1257,7 +1257,7 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - ENVIRONMENT: aws + ENVIRONMENT: test SCRIPT: run-oidc-unified-tests.sh args: - .evergreen/run-oidc-tests-test.sh diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 3c45cbd21ee..782de1779ef 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1210,7 +1210,7 @@ tasks: env: DRIVERS_TOOLS: ${DRIVERS_TOOLS} PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - ENVIRONMENT: aws + ENVIRONMENT: test SCRIPT: run-oidc-unified-tests.sh args: - .evergreen/run-oidc-tests-test.sh diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index f7f56f60535..6daf6262c65 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -119,6 +119,9 @@ export class MongoDBOIDC extends AuthProvider { */ override async auth(authContext: AuthContext): Promise { const { connection, reauthenticating, response } = authContext; + if (response?.speculativeAuthenticate?.done) { + return; + } const credentials = getCredentials(authContext); if (reauthenticating) { await this.workflow.reauthenticate(connection, credentials); diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 7893e471c14..9b29b3b6a56 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -1,6 +1,6 @@ -import { type Document } from 'bson'; import { setTimeout } from 'timers/promises'; +import { type Document } from '../../../bson'; import { type Connection } from '../../connection'; import { type MongoCredentials } from '../mongo_credentials'; import { diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index a2bc4a2d60a..87c4d8d94c8 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -63,7 +63,7 @@ async function getAzureTokenData(tokenAudience: string, username?: string): Prom }); if (response.status !== 200) { throw new MongoAzureError( - `Status code ${response.status} returned from the Azure endpoint. Response body ${response.body}` + `Status code ${response.status} returned from the Azure endpoint. Response body: ${response.body}` ); } return JSON.parse(response.body); diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index ab94a60cd70..031f2383c46 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -53,6 +53,10 @@ export abstract class MachineWorkflow implements Workflow { * Get the document to add for speculative authentication. */ async speculativeAuth(credentials: MongoCredentials): Promise { + // The spec states only cached access tokens can use speculative auth. + if (!this.cache.hasAccessToken) { + return {}; + } const token = await this.getTokenFromCacheOrEnv(credentials); const document = finishCommandDocument(token); document.db = credentials.source; diff --git a/test/tools/unified-spec-runner/entities.ts b/test/tools/unified-spec-runner/entities.ts index 673910bfa1f..654a1c9af64 100644 --- a/test/tools/unified-spec-runner/entities.ts +++ b/test/tools/unified-spec-runner/entities.ts @@ -369,7 +369,6 @@ export class FailPointMap extends Map { } else { // create a new client address = addressOrClient.toString(); - console.log('address', address); client = getClient(address, this.isSrv); try { await client.connect(); @@ -580,7 +579,6 @@ export class EntitiesMap extends Map { const useMultipleMongoses = (config.topologyType === 'LoadBalanced' || config.topologyType === 'Sharded') && entity.client.useMultipleMongoses; - console.log('client', process.env.MONGODB_URI, process.env.MONGODB_URI_SINGLE); let uri: string; // For OIDC we need to ensure we use MONGODB_URI_SINGLE for the MongoClient. if (process.env.MONGODB_URI_SINGLE?.includes('MONGODB-OIDC')) { @@ -589,17 +587,13 @@ export class EntitiesMap extends Map { uri = makeConnectionString(config.url({ useMultipleMongoses }), entity.client.uriOptions); } const client = new UnifiedMongoClient(uri, entity.client); - console.log(entity.client, uri); new EntityEventRegistry(client, entity.client, map).register(); try { - console.log('connecting'); await client.connect(); } catch (error) { - console.log('error', error); console.error(ejson`failed to connect entity ${entity}`); throw error; } - console.log('setting client', entity.client.id, client); map.set(entity.client.id, client); } else if ('database' in entity) { const client = map.getEntity('client', entity.database.client); diff --git a/test/tools/unified-spec-runner/runner.ts b/test/tools/unified-spec-runner/runner.ts index 4ea85a5c16c..721d8497ce6 100644 --- a/test/tools/unified-spec-runner/runner.ts +++ b/test/tools/unified-spec-runner/runner.ts @@ -220,11 +220,9 @@ async function runUnifiedTest( // If any event listeners were enabled on any client entities, // the test runner MUST now disable those event listeners. for (const [id, client] of entities.mapOf('client')) { - console.log('id, client', id, client); client.stopCapturingEvents(); clientList.set(id, client); } - console.log('clientList', clientList); if (test.expectEvents) { for (const expectedEventsForClient of test.expectEvents) { diff --git a/test/tools/unified-spec-runner/unified-utils.ts b/test/tools/unified-spec-runner/unified-utils.ts index 36868be13e3..4519a509839 100644 --- a/test/tools/unified-spec-runner/unified-utils.ts +++ b/test/tools/unified-spec-runner/unified-utils.ts @@ -210,24 +210,9 @@ export function makeConnectionString( ): string { const connectionString = new ConnectionString(uri); for (const [name, value] of Object.entries(uriOptions ?? {})) { - // 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 (process.env.AWS_WEB_IDENTITY_TOKEN_FILE) { - // connectionString.searchParams.set(name, 'ENVIRONMENT:aws'); - // } - // if (process.env.GCPOIDC_AUDIENCE) { - // connectionString.searchParams.set( - // name, - // `ENVIRONMENT:gcp,TOKEN_RESOURCE:${process.env.GCPOIDC_AUDIENCE}` - // ); - // } - // if (process.env.AZUREOIDC_CLIENTID) { - // connectionString.searchParams.set( - // name, - // `ENVIRONMENT:azure,TOKEN_RESOURCE:${process.env.AZUREOIDC_CLIENTID}` - // ); - // } + // This is a no-op - we want to ignore setting this as the URI in the + // environment already has the auth mech property set. } else { connectionString.searchParams.set(name, String(value)); } From 060338df77dbd694b0c3c361f1c4034e7ca580e7 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 5 May 2024 19:25:09 +0200 Subject: [PATCH 14/64] fix: lock all machine workflow token calls --- .../auth/mongodb_oidc/machine_workflow.ts | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index 031f2383c46..c3430c9ec2f 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -16,18 +16,23 @@ export interface AccessToken { expires_in?: number; } +/** @internal */ +export type OIDCTokenFunction = (credentials: MongoCredentials) => Promise; + /** * Common behaviour for OIDC machine workflows. * @internal */ export abstract class MachineWorkflow implements Workflow { cache: TokenCache; + callback: OIDCTokenFunction; /** * Instantiate the machine workflow. */ constructor(cache: TokenCache) { this.cache = cache; + this.callback = this.withLock(this.getToken.bind(this)); } /** @@ -70,12 +75,25 @@ export abstract class MachineWorkflow implements Workflow { if (this.cache.hasAccessToken) { return this.cache.getAccessToken(); } else { - const token = await this.getToken(credentials); + const token = await this.callback(credentials); this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); return token.access_token; } } + /** + * Ensure the callback is only executed one at a time. + */ + private withLock(callback: OIDCTokenFunction) { + let lock: Promise = Promise.resolve(); + return async (credentials: MongoCredentials): Promise => { + await lock; + // eslint-disable-next-line github/no-then + lock = lock.then(() => callback(credentials)); + return await lock; + }; + } + /** * Get the token from the environment or endpoint. */ From d19587d78b01e2d8fa0c6ba1ba81ceca35d6e1fb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 5 May 2024 19:40:29 +0200 Subject: [PATCH 15/64] refactor: use get in common utils --- src/client-side-encryption/providers/azure.ts | 2 +- src/client-side-encryption/providers/utils.ts | 37 ------------------- .../mongodb_oidc/azure_machine_workflow.ts | 4 +- .../auth/mongodb_oidc/gcp_machine_workflow.ts | 12 ++++-- src/utils.ts | 33 +++++++++++++++++ 5 files changed, 44 insertions(+), 44 deletions(-) delete mode 100644 src/client-side-encryption/providers/utils.ts diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index bee6038bdd8..1daf5852106 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -1,7 +1,7 @@ import { type Document } from '../../bson'; +import { get } from '../../utils'; import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; import { type KMSProviders } from './index'; -import { get } from './utils'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; diff --git a/src/client-side-encryption/providers/utils.ts b/src/client-side-encryption/providers/utils.ts deleted file mode 100644 index 8d5362c6993..00000000000 --- a/src/client-side-encryption/providers/utils.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as http from 'http'; -import { clearTimeout, setTimeout } from 'timers'; - -import { MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; - -/** - * @internal - */ -export function get( - url: URL | string, - options: http.RequestOptions = {} -): Promise<{ body: string; status: number | undefined }> { - return new Promise((resolve, reject) => { - /* eslint-disable prefer-const */ - let timeoutId: NodeJS.Timeout; - const request = http - .get(url, options, response => { - response.setEncoding('utf8'); - let body = ''; - response.on('data', chunk => (body += chunk)); - response.on('end', () => { - clearTimeout(timeoutId); - resolve({ status: response.statusCode, body }); - }); - }) - .on('error', error => { - clearTimeout(timeoutId); - reject(error); - }) - .end(); - timeoutId = setTimeout(() => { - request.destroy( - new MongoCryptKMSRequestNetworkTimeoutError(`request timed out after 10 seconds`) - ); - }, 10000); - }); -} diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 87c4d8d94c8..40df03fc055 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,5 +1,5 @@ -import { get } from '../../../client-side-encryption/providers/utils'; import { MongoAzureError } from '../../../error'; +import { get } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; import { type TokenCache } from './token_cache'; @@ -58,7 +58,7 @@ async function getAzureTokenData(tokenAudience: string, username?: string): Prom if (username) { url.searchParams.append('client_id', username); } - const response = await get(url.toString(), { + const response = await get(url, { headers: AZURE_HEADERS }); if (response.status !== 200) { diff --git a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts index f021ec35bf9..96c7ba635f6 100644 --- a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -1,5 +1,5 @@ import { MongoGCPError } from '../../../error'; -import { request } from '../../../utils'; +import { get } from '../../../utils'; import { type MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; import { type TokenCache } from './token_cache'; @@ -41,9 +41,13 @@ export class GCPMachineWorkflow extends MachineWorkflow { async function getGcpTokenData(tokenAudience: string): Promise { const url = new URL(GCP_BASE_URL); url.searchParams.append('audience', tokenAudience); - const data = await request(url.toString(), { - json: false, + const response = await get(url, { headers: GCP_HEADERS }); - return { access_token: data }; + if (response.status !== 200) { + throw new MongoGCPError( + `Status code ${response.status} returned from the GCP endpoint. Response body: ${response.body}` + ); + } + return JSON.parse(response.body); } diff --git a/src/utils.ts b/src/utils.ts index 57079b1f639..2ede778258d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,6 +3,7 @@ import type { SrvRecord } from 'dns'; import { type EventEmitter } from 'events'; import { promises as fs } from 'fs'; import * as http from 'http'; +import { clearTimeout, setTimeout } from 'timers'; import * as url from 'url'; import { URL } from 'url'; import { promisify } from 'util'; @@ -1157,6 +1158,38 @@ interface RequestOptions { headers?: http.OutgoingHttpHeaders; } +/** + * Perform a get request that returns status and body. + * @internal + */ +export function get( + url: URL | string, + options: http.RequestOptions = {} +): Promise<{ body: string; status: number | undefined }> { + return new Promise((resolve, reject) => { + /* eslint-disable prefer-const */ + let timeoutId: NodeJS.Timeout; + const request = http + .get(url, options, response => { + response.setEncoding('utf8'); + let body = ''; + response.on('data', chunk => (body += chunk)); + response.on('end', () => { + clearTimeout(timeoutId); + resolve({ status: response.statusCode, body }); + }); + }) + .on('error', error => { + clearTimeout(timeoutId); + reject(error); + }) + .end(); + timeoutId = setTimeout(() => { + request.destroy(new MongoNetworkTimeoutError(`request timed out after 10 seconds`)); + }, 10000); + }); +} + export async function request(uri: string): Promise>; export async function request( uri: string, From d4211d5221ee67736b1a17f5868681358a354003 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 5 May 2024 20:11:15 +0200 Subject: [PATCH 16/64] test: remaining prose tests --- package.json | 6 +- ...ose.test.ts => mongodb_oidc.prose.test.ts} | 145 ++++++++++++++++++ ...ts => mongodb_oidc_azure.prose.05.test.ts} | 0 ...t.ts => mongodb_oidc_gcp.prose.06.test.ts} | 0 4 files changed, 148 insertions(+), 3 deletions(-) rename test/integration/auth/{mongodb_oidc_test.prose.test.ts => mongodb_oidc.prose.test.ts} (89%) rename test/integration/auth/{mongodb_oidc_azure.prose.test.ts => mongodb_oidc_azure.prose.05.test.ts} (100%) rename test/integration/auth/{mongodb_oidc_gcp.prose.test.ts => mongodb_oidc_gcp.prose.06.test.ts} (100%) diff --git a/package.json b/package.json index f859cd51ae5..2c045e09186 100644 --- a/package.json +++ b/package.json @@ -149,9 +149,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-auth": "mocha --config test/mocha_mongodb.json test/integration/auth/auth.spec.test.ts", - "check:oidc-test": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_test.prose.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:oidc-test": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc.prose.test.ts", + "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.05.test.ts", + "check:oidc-gcp": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_gcp.prose.06.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/test/integration/auth/mongodb_oidc_test.prose.test.ts b/test/integration/auth/mongodb_oidc.prose.test.ts similarity index 89% rename from test/integration/auth/mongodb_oidc_test.prose.test.ts rename to test/integration/auth/mongodb_oidc.prose.test.ts index a788f6b9e30..f60c17094e9 100644 --- a/test/integration/auth/mongodb_oidc_test.prose.test.ts +++ b/test/integration/auth/mongodb_oidc.prose.test.ts @@ -1155,6 +1155,151 @@ describe('OIDC Auth Spec Tests', function () { expect(commandFailedEvents).to.deep.equal(['find']); }); }); + + describe('4.2 Succeeds no refresh', function () { + let utilClient: MongoClient; + const callbackSpy = sinon.spy(createCallback()); + // Create an OIDC configured client with a human callback that does not return a refresh token. + // Perform a find operation that succeeds. + // Assert that the human callback has been called once. + // Force a reauthenication using a fail point of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find" + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that succeeds. + // Assert that the human callback has been called twice. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + monitorCommands: true, + retryReads: false + }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + await utilClient + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await utilClient.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await utilClient.close(); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + }); + }); + + describe('4.3 Succeeds after refresh fails', function () { + const createBadCallback = () => { + return async () => { + const token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { + encoding: 'utf8' + }); + return generateResult(token, 10000, { refreshToken: 'bad' }); + }; + }; + + let utilClient: MongoClient; + const callbackSpy = sinon.spy(createBadCallback()); + // Create an OIDC configured client with a callback that returns the test_user1 access token and a bad refresh token. + // Perform a find operation that succeeds. + // Assert that the human callback has been called once. + // Force a reauthenication using a fail point of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find", + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that succeeds. + // Assert that the human callback has been called 2 times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + monitorCommands: true, + retryReads: false + }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + await utilClient + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await utilClient.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await utilClient.close(); + }); + + it('successfully authenticates', async function () { + await collection.findOne(); + expect(callbackSpy).to.have.been.calledTwice; + }); + }); }); }); }); diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.05.test.ts similarity index 100% rename from test/integration/auth/mongodb_oidc_azure.prose.test.ts rename to test/integration/auth/mongodb_oidc_azure.prose.05.test.ts diff --git a/test/integration/auth/mongodb_oidc_gcp.prose.test.ts b/test/integration/auth/mongodb_oidc_gcp.prose.06.test.ts similarity index 100% rename from test/integration/auth/mongodb_oidc_gcp.prose.test.ts rename to test/integration/auth/mongodb_oidc_gcp.prose.06.test.ts From 6ace5305b0dd53344c518fd2fa4fa02fe6c02925 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 5 May 2024 20:40:19 +0200 Subject: [PATCH 17/64] test: last prose test --- .../auth/mongodb_oidc.prose.test.ts | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/test/integration/auth/mongodb_oidc.prose.test.ts b/test/integration/auth/mongodb_oidc.prose.test.ts index f60c17094e9..9f61597f527 100644 --- a/test/integration/auth/mongodb_oidc.prose.test.ts +++ b/test/integration/auth/mongodb_oidc.prose.test.ts @@ -1300,6 +1300,92 @@ describe('OIDC Auth Spec Tests', function () { expect(callbackSpy).to.have.been.calledTwice; }); }); + + describe('4.4 Fails', function () { + let accessCount = 0; + + const createBadCallback = () => { + return async () => { + let token; + if (accessCount === 0) { + token = await readFile(path.join(process.env.OIDC_TOKEN_DIR, 'test_user1'), { + encoding: 'utf8' + }); + } else { + token = 'bad'; + } + accessCount++; + return generateResult(token, 10000, { refreshToken: 'bad' }); + }; + }; + + let utilClient: MongoClient; + const callbackSpy = sinon.spy(createBadCallback()); + // Create an OIDC configured client that returns invalid refresh tokens and returns invalid access tokens after the first access. + // Perform a find operation that succeeds. + // Assert that the human callback has been called once. + // Force a reauthenication using a failCommand of the form: + // { + // configureFailPoint: "failCommand", + // mode: { + // times: 1 + // }, + // data: { + // failCommands: [ + // "find", + // ], + // errorCode: 391 // ReauthenticationRequired + // } + // } + // Perform a find operation that fails. + // Assert that the human callback has been called three times. + // Close the client. + beforeEach(async function () { + client = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: callbackSpy + }, + monitorCommands: true, + retryReads: false + }); + utilClient = new MongoClient(uriSingle, { + authMechanismProperties: { + OIDC_HUMAN_CALLBACK: createCallback() + }, + retryReads: false + }); + collection = client.db('test').collection('testHuman'); + await collection.findOne(); + expect(callbackSpy).to.have.been.calledOnce; + await utilClient + .db() + .admin() + .command({ + configureFailPoint: 'failCommand', + mode: { + times: 1 + }, + data: { + failCommands: ['find'], + errorCode: 391 + } + }); + }); + + afterEach(async function () { + await utilClient.db().admin().command({ + configureFailPoint: 'failCommand', + mode: 'off' + }); + await utilClient.close(); + }); + + it('does not successfully authenticate', async function () { + const error = await collection.findOne().catch(error => error); + expect(error).to.exist; + expect(callbackSpy).to.have.been.calledThrice; + }); + }); }); }); }); From 8e74bc907d466b336be5fd36ab28b072daada1ae Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 5 May 2024 21:23:27 +0200 Subject: [PATCH 18/64] refactor: update workflow interface to void when needed --- src/cmap/auth/mongodb_oidc.ts | 4 ++-- .../mongodb_oidc/automated_callback_workflow.ts | 15 +++++++++------ src/cmap/auth/mongodb_oidc/callback_workflow.ts | 9 ++++----- .../auth/mongodb_oidc/human_callback_workflow.ts | 8 ++++---- src/cmap/auth/mongodb_oidc/machine_workflow.ts | 8 ++++---- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 6daf6262c65..f192f9d5d7f 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -78,12 +78,12 @@ export interface Workflow { connection: Connection, credentials: MongoCredentials, response?: Document - ): Promise; + ): Promise; /** * Each workflow should specify the correct custom behaviour for reauthentication. */ - reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; + reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; /** * Get the document to add for speculative authentication. diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 9b29b3b6a56..1fee9a502cc 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -1,6 +1,6 @@ import { setTimeout } from 'timers/promises'; -import { type Document } from '../../../bson'; +import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; import { type Connection } from '../../connection'; import { type MongoCredentials } from '../mongo_credentials'; import { @@ -35,16 +35,16 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { * in the cache and run the authentication steps again. No initial handshake needs * to be sent. */ - async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token. this.cache.removeAccessToken(); - return await this.execute(connection, credentials); + await this.execute(connection, credentials); } /** * Execute the OIDC callback workflow. */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { + async execute(connection: Connection, credentials: MongoCredentials): Promise { // If there is a cached access token, try to authenticate with it. If // authentication fails with an Authentication error (18), // invalidate the access token, fetch a new access token, and try @@ -55,7 +55,10 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { try { return await this.finishAuthentication(connection, credentials, token); } catch (error) { - if (error.code === 18) { + if ( + error instanceof MongoError && + error.code === MONGODB_ERROR_CODES.AuthenticationFailed + ) { this.cache.removeAccessToken(); return await this.execute(connection, credentials); } else { @@ -77,7 +80,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } this.lastInvocationTime = now; this.cache.put(response); - return await this.finishAuthentication(connection, credentials, response.accessToken); + await this.finishAuthentication(connection, credentials, response.accessToken); } /** diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index c2b5569ea62..c628c0a7aa3 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -60,7 +60,7 @@ export abstract class CallbackWorkflow implements Workflow { /** * Each workflow should specify the correct custom behaviour for reauthentication. */ - abstract reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; + abstract reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; /** * Execute the OIDC callback workflow. @@ -69,7 +69,7 @@ export abstract class CallbackWorkflow implements Workflow { connection: Connection, credentials: MongoCredentials, response?: Document - ): Promise; + ): Promise; /** * Starts the callback authentication process. If there is a speculative @@ -102,13 +102,12 @@ export abstract class CallbackWorkflow implements Workflow { credentials: MongoCredentials, token: string, conversationId?: number - ): Promise { - const result = await connection.command( + ): Promise { + await connection.command( ns(credentials.source), finishCommandDocument(token, conversationId), undefined ); - return result; } /** diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index d09a3dd50e7..e99fa822a40 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -1,4 +1,4 @@ -import { BSON, type Document } from 'bson'; +import { BSON } from 'bson'; import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; import { type Connection } from '../../connection'; @@ -30,18 +30,18 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { * in the cache and run the authentication steps again. No initial handshake needs * to be sent. */ - async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication should always remove the access token, but in the // human workflow we need to pass the refesh token through if it // exists. this.cache.removeAccessToken(); - return await this.execute(connection, credentials); + await this.execute(connection, credentials); } /** * Execute the OIDC human callback workflow. */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { + async execute(connection: Connection, credentials: MongoCredentials): Promise { // Check if the Client Cache has an access token. // If it does, cache the access token in the Connection Cache and perform a One-Step SASL conversation // using the access token. If the server returns an Authentication error (18), diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index c3430c9ec2f..d131fe0bc03 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -38,20 +38,20 @@ export abstract class MachineWorkflow implements Workflow { /** * Execute the workflow. Gets the token from the subclass implementation. */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { + async execute(connection: Connection, credentials: MongoCredentials): Promise { const token = await this.getTokenFromCacheOrEnv(credentials); const command = finishCommandDocument(token); - return await connection.command(ns(credentials.source), command, undefined); + await 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): Promise { + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { // Reauthentication implies the token has expired. this.cache.removeAccessToken(); - return await this.execute(connection, credentials); + await this.execute(connection, credentials); } /** From 7e6266830e1dc8946b5ff557f926cb9abc4db945 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 15:29:00 +0200 Subject: [PATCH 19/64] fix: more comment addressing --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 5 +---- src/index.ts | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index c628c0a7aa3..52cda7956d6 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,6 +1,6 @@ import { type Document } from 'bson'; -import { MongoDriverError, MongoMissingCredentialsError } from '../../../error'; +import { MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; import type { MongoCredentials } from '../mongo_credentials'; @@ -114,9 +114,6 @@ export abstract class CallbackWorkflow implements Workflow { * Executes the callback and validates the output. */ protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise { - if (!this.callback) { - throw new MongoDriverError(''); - } // With no token in the cache we use the request callback. const result = await this.callback(params); // Validate that the result returned by the callback is acceptable. If it is not diff --git a/src/index.ts b/src/index.ts index c5422f68b55..302cc23b36b 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,7 +101,6 @@ export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; export { Workflow } from './cmap/auth/mongodb_oidc'; -export type { 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'; @@ -259,6 +258,7 @@ export type { OIDCCallbackParams, OIDCResponse } from './cmap/auth/mongodb_oidc'; +export type { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export type { MessageHeader, OpCompressedRequest, From 00e259df1429472f5085a9fab2cae8e000b9371a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 15:45:57 +0200 Subject: [PATCH 20/64] fix: use timeout as number --- src/cmap/auth/mongodb_oidc.ts | 2 +- src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts | 2 +- src/cmap/auth/mongodb_oidc/human_callback_workflow.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index f192f9d5d7f..8b8873cb029 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -51,7 +51,7 @@ export interface OIDCResponse { * @public */ export interface OIDCCallbackParams { - timeoutContext: AbortSignal; + callbackTimeoutMS: number; version: 1; idpInfo?: IdPInfo; refreshToken?: string; diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 1fee9a502cc..8fcb66310a9 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -88,7 +88,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { */ protected async fetchAccessToken(): Promise { const params: OIDCCallbackParams = { - timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), + callbackTimeoutMS: AUTOMATED_TIMEOUT_MS, version: OIDC_VERSION }; return await this.executeAndValidateCallback(params); diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index e99fa822a40..3200ca1fed2 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -114,7 +114,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { */ private async fetchAccessToken(idpInfo: IdPInfo, refreshToken?: string): Promise { const params: OIDCCallbackParams = { - timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), + callbackTimeoutMS: HUMAN_TIMEOUT_MS, version: OIDC_VERSION, idpInfo: idpInfo }; From b67a92a3243f36849141ccbacd7a82f62fb350b6 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 15:54:02 +0200 Subject: [PATCH 21/64] docs: update version comment --- src/cmap/auth/mongodb_oidc.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 8b8873cb029..bb5c9389f2f 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -48,6 +48,10 @@ export interface OIDCResponse { /** * The parameters that the driver provides to the user supplied * human or machine callback. + * + * The version number is used to communicate callback API changes that are not breaking but that + * users may want to know about and review their implementation. Users may wish to check the version + * number and throw an error if their expected version number and the one provided do not match. * @public */ export interface OIDCCallbackParams { From d37bdcffed6256269a36b0ad425898c076b8d5cf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 16:15:32 +0200 Subject: [PATCH 22/64] Revert "fix: use timeout as number" This reverts commit 23e2bc34be8a81fc7b650757af7624d0c906cee9. --- src/cmap/auth/mongodb_oidc.ts | 2 +- src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts | 2 +- src/cmap/auth/mongodb_oidc/human_callback_workflow.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index bb5c9389f2f..70c54039a93 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -55,7 +55,7 @@ export interface OIDCResponse { * @public */ export interface OIDCCallbackParams { - callbackTimeoutMS: number; + timeoutContext: AbortSignal; version: 1; idpInfo?: IdPInfo; refreshToken?: string; diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 8fcb66310a9..1fee9a502cc 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -88,7 +88,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { */ protected async fetchAccessToken(): Promise { const params: OIDCCallbackParams = { - callbackTimeoutMS: AUTOMATED_TIMEOUT_MS, + timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), version: OIDC_VERSION }; return await this.executeAndValidateCallback(params); diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index 3200ca1fed2..e99fa822a40 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -114,7 +114,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { */ private async fetchAccessToken(idpInfo: IdPInfo, refreshToken?: string): Promise { const params: OIDCCallbackParams = { - callbackTimeoutMS: HUMAN_TIMEOUT_MS, + timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), version: OIDC_VERSION, idpInfo: idpInfo }; From be8ba66d99b2cd69be57368ac8a7811bb8c800af Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 18:22:38 +0200 Subject: [PATCH 23/64] docs: document all the APIs --- src/cmap/auth/mongo_credentials.ts | 6 +++++- src/cmap/auth/mongodb_oidc.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index f6e2ecfbcde..d77de6dbf38 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -61,12 +61,16 @@ export interface AuthMechanismProperties extends Document { SERVICE_REALM?: string; CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue; AWS_SESSION_TOKEN?: string; + /** A user provided OIDC machine callback function. */ OIDC_CALLBACK?: OIDCCallbackFunction; + /** A user provided OIDC human interacted callback function. */ OIDC_HUMAN_CALLBACK?: OIDCCallbackFunction; + /** The OIDC environment. Note that 'test' is for internal use only. */ ENVIRONMENT?: 'test' | 'azure' | 'gcp'; + /** Allowed hosts that OIDC auth can connect to. */ ALLOWED_HOSTS?: string[]; + /** The resource token for OIDC auth in Azure and GCP. */ TOKEN_RESOURCE?: string; - TOKEN_CLIENT_ID?: string; } /** @public */ diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 70c54039a93..fdfb929f1d4 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -18,8 +18,15 @@ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; * @public */ export interface IdPInfo { + /** + * A URL which describes the Authentication Server. This identifier should + * be the iss of provided access tokens, and be viable for RFC8414 metadata + * discovery and RFC9207 identification. + */ issuer: string; + /** A unique client ID for this OIDC client. */ clientId: string; + /** A list of additional scopes to request from IdP. */ requestScopes?: string[]; } @@ -29,8 +36,11 @@ export interface IdPInfo { * @public */ export interface IdPServerResponse { + /** The OIDC access token. */ accessToken: string; + /** The time when the access token expires. For future use. */ expiresInSeconds?: number; + /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */ refreshToken?: string; } @@ -40,8 +50,11 @@ export interface IdPServerResponse { * @public */ export interface OIDCResponse { + /** The OIDC access token. */ accessToken: string; + /** The time when the access token expires. For future use. */ expiresInSeconds?: number; + /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */ refreshToken?: string; } @@ -55,9 +68,15 @@ export interface OIDCResponse { * @public */ export interface OIDCCallbackParams { + /** Optional username. */ + username?: string; + /** The context in which to timeout the OIDC callback. */ timeoutContext: AbortSignal; + /** The current OIDC API version. */ version: 1; + /** The IdP information returned from the server. */ idpInfo?: IdPInfo; + /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */ refreshToken?: string; } From 6514ab250528a380e45dbe8cec59b47e44dcecdb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 18:25:23 +0200 Subject: [PATCH 24/64] fix: forgot to pass username --- .../mongodb_oidc/automated_callback_workflow.ts | 9 ++++++--- .../mongodb_oidc/human_callback_workflow.ts | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 1fee9a502cc..9ea6ec80c1c 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -70,11 +70,11 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { const now = Date.now(); // Ensure a delay between invokations to not overload the callback. if (now - this.lastInvocationTime > CALLBACK_DEBOUNCE_MS) { - response = await this.fetchAccessToken(); + response = await this.fetchAccessToken(credentials); } else { const responses = await Promise.all([ setTimeout(CALLBACK_DEBOUNCE_MS - (now - this.lastInvocationTime)), - this.fetchAccessToken() + this.fetchAccessToken(credentials) ]); response = responses[1]; } @@ -86,11 +86,14 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { /** * Fetches the access token using the callback. */ - protected async fetchAccessToken(): Promise { + protected async fetchAccessToken(credentials: MongoCredentials): Promise { const params: OIDCCallbackParams = { timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), version: OIDC_VERSION }; + if (credentials.username) { + params.username = credentials.username; + } return await this.executeAndValidateCallback(params); } } diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index e99fa822a40..7ee048e2fd6 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -72,7 +72,11 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { // errors to the user. On success, exit the algorithm. if (this.cache.hasRefreshToken) { const refreshToken = this.cache.getRefreshToken(); - const result = await this.fetchAccessToken(this.cache.getIdpInfo(), refreshToken); + const result = await this.fetchAccessToken( + this.cache.getIdpInfo(), + credentials, + refreshToken + ); this.cache.put(result); try { return await this.finishAuthentication(connection, credentials, result.accessToken); @@ -99,7 +103,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { const startResponse = await this.startAuthentication(connection, credentials); const conversationId = startResponse.conversationId; const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo; - const callbackResponse = await this.fetchAccessToken(idpInfo); + const callbackResponse = await this.fetchAccessToken(idpInfo, credentials); this.cache.put(callbackResponse, idpInfo); return await this.finishAuthentication( connection, @@ -112,12 +116,19 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { /** * Fetches an access token using the callback. */ - private async fetchAccessToken(idpInfo: IdPInfo, refreshToken?: string): Promise { + private async fetchAccessToken( + idpInfo: IdPInfo, + credentials: MongoCredentials, + refreshToken?: string + ): Promise { const params: OIDCCallbackParams = { timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), version: OIDC_VERSION, idpInfo: idpInfo }; + if (credentials.username) { + params.username = credentials.username; + } if (refreshToken) { params.refreshToken = refreshToken; } From 0aacf514468fd59cfa24b5bb5b08b5ea9811d26d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 18:35:38 +0200 Subject: [PATCH 25/64] docs: move comment --- src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index 9ea6ec80c1c..bcb37995289 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -68,10 +68,10 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } let response: OIDCResponse; const now = Date.now(); - // Ensure a delay between invokations to not overload the callback. if (now - this.lastInvocationTime > CALLBACK_DEBOUNCE_MS) { response = await this.fetchAccessToken(credentials); } else { + // Ensure a delay between invokations to not overload the callback. const responses = await Promise.all([ setTimeout(CALLBACK_DEBOUNCE_MS - (now - this.lastInvocationTime)), this.fetchAccessToken(credentials) From 30d8601afc82c8c0c5c9801036b1a8a1da6cac79 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 21:04:36 +0200 Subject: [PATCH 26/64] refactor: locking and timeouts --- .../automated_callback_workflow.ts | 18 ++++++++++-- .../auth/mongodb_oidc/callback_workflow.ts | 5 ++-- .../mongodb_oidc/human_callback_workflow.ts | 18 ++++++++++-- src/error.ts | 28 +++++++++++++++++++ 4 files changed, 61 insertions(+), 8 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index bcb37995289..d001a55bff4 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -1,6 +1,7 @@ import { setTimeout } from 'timers/promises'; -import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; +import { MONGODB_ERROR_CODES, MongoError, MongoOIDCError } from '../../../error'; +import { Timeout, TimeoutError } from '../../../timeout'; import { type Connection } from '../../connection'; import { type MongoCredentials } from '../mongo_credentials'; import { @@ -87,13 +88,24 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { * Fetches the access token using the callback. */ protected async fetchAccessToken(credentials: MongoCredentials): Promise { + const controller = new AbortController(); const params: OIDCCallbackParams = { - timeoutContext: AbortSignal.timeout(AUTOMATED_TIMEOUT_MS), + timeoutContext: controller.signal, version: OIDC_VERSION }; if (credentials.username) { params.username = credentials.username; } - return await this.executeAndValidateCallback(params); + const timeout = Timeout.expires(AUTOMATED_TIMEOUT_MS); + try { + return await Promise.race([this.executeAndValidateCallback(params), timeout]); + } catch (error) { + if (TimeoutError.is(error)) { + timeout.clear(); + controller.abort(); + throw new MongoOIDCError(`OIDC callback timed out after ${AUTOMATED_TIMEOUT_MS}ms.`); + } + throw error; + } } } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 52cda7956d6..a8e23234efe 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -130,9 +130,10 @@ export abstract class CallbackWorkflow implements Workflow { protected withLock(callback: OIDCCallbackFunction) { let lock: Promise = Promise.resolve(); return async (params: OIDCCallbackParams): Promise => { + // We do this to ensure that we would never return the result of the + // previous lock, only the current callback's value would get returned. await lock; - // eslint-disable-next-line github/no-then - lock = lock.then(() => callback(params)); + lock = callback(params); return await lock; }; } diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index 7ee048e2fd6..a4b5ff17dec 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -1,6 +1,7 @@ import { BSON } from 'bson'; -import { MONGODB_ERROR_CODES, MongoError } from '../../../error'; +import { MONGODB_ERROR_CODES, MongoError, MongoOIDCError } from '../../../error'; +import { Timeout, TimeoutError } from '../../../timeout'; import { type Connection } from '../../connection'; import { type MongoCredentials } from '../mongo_credentials'; import { @@ -121,8 +122,9 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { credentials: MongoCredentials, refreshToken?: string ): Promise { + const controller = new AbortController(); const params: OIDCCallbackParams = { - timeoutContext: AbortSignal.timeout(HUMAN_TIMEOUT_MS), + timeoutContext: controller.signal, version: OIDC_VERSION, idpInfo: idpInfo }; @@ -132,6 +134,16 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { if (refreshToken) { params.refreshToken = refreshToken; } - return await this.executeAndValidateCallback(params); + const timeout = Timeout.expires(HUMAN_TIMEOUT_MS); + try { + return await Promise.race([this.executeAndValidateCallback(params), timeout]); + } catch (error) { + if (TimeoutError.is(error)) { + timeout.clear(); + controller.abort(); + throw new MongoOIDCError(`OIDC callback timed out after ${HUMAN_TIMEOUT_MS}ms.`); + } + throw error; + } } } diff --git a/src/error.ts b/src/error.ts index 024147d1815..f198fe88b5a 100644 --- a/src/error.ts +++ b/src/error.ts @@ -530,6 +530,34 @@ export class MongoAWSError extends MongoRuntimeError { } } +/** + * A error generated when the user attempts to authenticate + * via OIDC callbacks, but fails. + * + * @public + * @category Error + */ +export class MongoOIDCError 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 'MongoOIDCError'; + } +} + /** * A error generated when the user attempts to authenticate * via Azure, but fails. From e85d177a3d0617bc9e91ee83725295497a9fe557 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 21:13:30 +0200 Subject: [PATCH 27/64] refactor: clear timeout in finally --- src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts | 3 ++- src/cmap/auth/mongodb_oidc/human_callback_workflow.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index d001a55bff4..cbaa14a56ce 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -101,11 +101,12 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { return await Promise.race([this.executeAndValidateCallback(params), timeout]); } catch (error) { if (TimeoutError.is(error)) { - timeout.clear(); controller.abort(); throw new MongoOIDCError(`OIDC callback timed out after ${AUTOMATED_TIMEOUT_MS}ms.`); } throw error; + } finally { + timeout.clear(); } } } diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index a4b5ff17dec..8de5ad3637b 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -139,11 +139,12 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { return await Promise.race([this.executeAndValidateCallback(params), timeout]); } catch (error) { if (TimeoutError.is(error)) { - timeout.clear(); controller.abort(); throw new MongoOIDCError(`OIDC callback timed out after ${HUMAN_TIMEOUT_MS}ms.`); } throw error; + } finally { + timeout.clear(); } } } From 9aa10c2faaafe925c0d32347c6e69a9cc5369108 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 21:36:38 +0200 Subject: [PATCH 28/64] test: fix kms test --- src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts | 2 +- ...ent_side_encryption.prose.18.azure_kms_mock_server.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts index 96c7ba635f6..6b8c1ee0541 100644 --- a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -49,5 +49,5 @@ async function getGcpTokenData(tokenAudience: string): Promise { `Status code ${response.status} returned from the GCP endpoint. Response body: ${response.body}` ); } - return JSON.parse(response.body); + return { access_token: response.body }; } diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts index c99820b6f83..5f092b0f111 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts @@ -7,7 +7,7 @@ import { type AzureKMSRequestOptions, fetchAzureKMSToken } from '../../../src/client-side-encryption/providers/azure'; -import { type Document } from '../../mongodb'; +import { type Document, MongoNetworkTimeoutError } from '../../mongodb'; const BASE_URL = new URL(`http://127.0.0.1:8080/metadata/identity/oauth2/token`); class KMSRequestOptions implements AzureKMSRequestOptions { @@ -119,7 +119,7 @@ context('Azure KMS Mock Server Tests', function () { it('returns an error after the request times out', async () => { const error = await fetchAzureKMSToken(new KMSRequestOptions('slow')).catch(e => e); - expect(error).to.be.instanceof(MongoCryptAzureKMSRequestError); + expect(error).to.be.instanceof(MongoNetworkTimeoutError); }); }); }); From c99f750204dbc77cf43182fdf266734339ed2baa Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 6 May 2024 23:09:17 +0200 Subject: [PATCH 29/64] test: fix fle unit test --- .../providers/credentialsProvider.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index 486fb41c60e..03343c398bb 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -19,9 +19,9 @@ import { tokenCache } from '../../../../src/client-side-encryption/providers/azure'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import * as utils from '../../../../src/client-side-encryption/providers/utils'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports import { AWSSDKCredentialProvider } from '../../../../src/cmap/auth/aws_temporary_credentials'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import * as utils from '../../../../src/utils'; import * as requirements from '../requirements.helper'; const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; From fef5d55cacb6fa327ebc6bce9896d7d83158b576 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 15:58:46 +0200 Subject: [PATCH 30/64] test: fix unit tests --- src/index.ts | 2 +- test/unit/cmap/auth/mongodb_oidc.test.ts | 51 ------------------------ test/unit/connection_string.test.ts | 14 +++---- 3 files changed, 8 insertions(+), 59 deletions(-) delete mode 100644 test/unit/cmap/auth/mongodb_oidc.test.ts diff --git a/src/index.ts b/src/index.ts index 302cc23b36b..712b49a1629 100644 --- a/src/index.ts +++ b/src/index.ts @@ -101,6 +101,7 @@ export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; export { Workflow } from './cmap/auth/mongodb_oidc'; +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'; @@ -258,7 +259,6 @@ export type { OIDCCallbackParams, OIDCResponse } from './cmap/auth/mongodb_oidc'; -export type { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export type { MessageHeader, OpCompressedRequest, diff --git a/test/unit/cmap/auth/mongodb_oidc.test.ts b/test/unit/cmap/auth/mongodb_oidc.test.ts deleted file mode 100644 index 555bdb42ba9..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc.test.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { expect } from 'chai'; - -import { - AuthContext, - MongoCredentials, - MongoDBOIDC, - MongoInvalidArgumentError -} from '../../../mongodb'; - -describe('class MongoDBOIDC', () => { - context('when an unknown OIDC provider name is set', () => { - it('prepare rejects with MongoInvalidArgumentError', async () => { - const oidc = new MongoDBOIDC(); - const error = await oidc - .prepare( - {}, - new AuthContext( - {}, - new MongoCredentials({ - mechanism: 'MONGODB-OIDC', - mechanismProperties: { ENVIRONMENT: 'iLoveJavaScript' } - }), - {} - ) - ) - .catch(error => error); - - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error).to.match(/workflow for provider/); - }); - - it('auth rejects with MongoInvalidArgumentError', async () => { - const oidc = new MongoDBOIDC(); - const error = await oidc - .auth( - new AuthContext( - {}, - new MongoCredentials({ - mechanism: 'MONGODB-OIDC', - mechanismProperties: { ENVIRONMENT: 'iLoveJavaScript' } - }), - {} - ) - ) - .catch(error => error); - - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error).to.match(/workflow for provider/); - }); - }); -}); diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index a1bd6f1f603..244273ef789 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -303,7 +303,7 @@ describe('Connection String', function () { it('raises an error', function () { expect(() => { parseOptions( - 'mongodb://localhost/?authMechanismProperties=ENVIRONMENT:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' + 'mongodb://localhost/?authMechanismProperties=ENVIRONMENT:test,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' ); }).to.throw( MongoParseError, @@ -318,7 +318,7 @@ describe('Connection String', function () { it('sets the allowed hosts property', function () { const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test', { authMechanismProperties: { ALLOWED_HOSTS: hosts @@ -326,7 +326,7 @@ describe('Connection String', function () { } ); expect(options.credentials.mechanismProperties).to.deep.equal({ - ENVIRONMENT: 'aws', + ENVIRONMENT: 'test', ALLOWED_HOSTS: hosts }); }); @@ -336,7 +336,7 @@ describe('Connection String', function () { it('raises an error', function () { expect(() => { parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws', + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test', { authMechanismProperties: { ALLOWED_HOSTS: [1, 2, 3] @@ -354,10 +354,10 @@ describe('Connection String', function () { context('when ALLOWED_HOSTS is not in the options', function () { it('sets the default value', function () { const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:aws' + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:test' ); expect(options.credentials.mechanismProperties).to.deep.equal({ - ENVIRONMENT: 'aws', + ENVIRONMENT: 'test', ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS }); }); @@ -655,7 +655,7 @@ describe('Connection String', function () { makeStub('authSource=thisShouldNotBeAuthSource'); const mechanismProperties = {}; if (mechanism === AuthMechanism.MONGODB_OIDC) { - mechanismProperties.ENVIRONMENT = 'aws'; + mechanismProperties.ENVIRONMENT = 'test'; } const credentials = new MongoCredentials({ From 6dad21cb30b76f51f538049a75065489d6126d02 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 16:12:50 +0200 Subject: [PATCH 31/64] fix: type exports --- src/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 712b49a1629..3d491f1253e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -100,8 +100,6 @@ export { export { BatchType } from './bulk/common'; export { AutoEncryptionLoggerLevel } from './client-side-encryption/auto_encrypter'; export { GSSAPICanonicalizationValue } from './cmap/auth/gssapi'; -export { Workflow } from './cmap/auth/mongodb_oidc'; -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'; @@ -259,6 +257,8 @@ export type { OIDCCallbackParams, OIDCResponse } from './cmap/auth/mongodb_oidc'; +export { Workflow } from './cmap/auth/mongodb_oidc'; +export { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export type { MessageHeader, OpCompressedRequest, From 5c7efde57f2e9a7de1be65fde8a3ad35c32325b5 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 20:35:36 +0200 Subject: [PATCH 32/64] test: fix tests --- src/cmap/auth/mongo_credentials.ts | 13 +++++++++++++ src/index.ts | 1 + test/tools/uri_spec_runner.ts | 12 ++++++++++-- test/unit/assorted/auth.spec.test.ts | 5 +++++ test/unit/index.test.ts | 2 ++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index d77de6dbf38..aa89c56c590 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -136,6 +136,13 @@ export class MongoCredentials { }; } + if (this.mechanism === AuthMechanism.MONGODB_OIDC && this.mechanismProperties.TOKEN_RESOURCE) { + this.mechanismProperties = { + ...this.mechanismProperties, + TOKEN_RESOURCE: decodeURIComponent(this.mechanismProperties.TOKEN_RESOURCE) + }; + } + Object.freeze(this.mechanismProperties); Object.freeze(this); } @@ -194,6 +201,12 @@ export class MongoCredentials { ); } + if (this.username && this.password) { + throw new MongoInvalidArgumentError( + `No password is allowed in ENVIRONMENT '${this.mechanismProperties.ENVIRONMENT}' for '${this.mechanism}'.` + ); + } + if ( (this.mechanismProperties.ENVIRONMENT === 'azure' || this.mechanismProperties.ENVIRONMENT === 'gcp') && diff --git a/src/index.ts b/src/index.ts index 3d491f1253e..8213e31f88f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -62,6 +62,7 @@ export { MongoNetworkError, MongoNetworkTimeoutError, MongoNotConnectedError, + MongoOIDCError, MongoParseError, MongoRuntimeError, MongoServerClosedError, diff --git a/test/tools/uri_spec_runner.ts b/test/tools/uri_spec_runner.ts index 60c6dd89ba5..17ae4d65786 100644 --- a/test/tools/uri_spec_runner.ts +++ b/test/tools/uri_spec_runner.ts @@ -1,6 +1,12 @@ import { expect } from 'chai'; -import { MongoAPIError, MongoClient, MongoParseError, MongoRuntimeError } from '../mongodb'; +import { + MongoAPIError, + MongoAzureError, + MongoClient, + MongoParseError, + MongoRuntimeError +} from '../mongodb'; type HostObject = { type: 'ipv4' | 'ip_literal' | 'hostname' | 'unix'; @@ -69,7 +75,9 @@ export function executeUriValidationTest( new MongoClient(test.uri); expect.fail(`Expected "${test.uri}" to be invalid${test.valid ? ' because of warning' : ''}`); } catch (err) { - if (err instanceof MongoRuntimeError) { + if (err instanceof MongoAzureError) { + // Azure URI errors don't have an underlying cause. + } else if (err instanceof MongoRuntimeError) { expect(err).to.have.nested.property('cause.code').equal('ERR_INVALID_URL'); } else if ( // most of our validation is MongoParseError, which does not extend from MongoAPIError diff --git a/test/unit/assorted/auth.spec.test.ts b/test/unit/assorted/auth.spec.test.ts index c474fd8cf11..7553851c972 100644 --- a/test/unit/assorted/auth.spec.test.ts +++ b/test/unit/assorted/auth.spec.test.ts @@ -1,6 +1,8 @@ import { loadSpecTests } from '../../spec'; import { executeUriValidationTest } from '../../tools/uri_spec_runner'; +const SKIP = 'should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)'; + describe('Auth option spec tests (legacy)', function () { const suites = loadSpecTests('auth', 'legacy'); @@ -8,6 +10,9 @@ describe('Auth option spec tests (legacy)', function () { describe(suite.name, function () { for (const test of suite.tests) { it(`${test.description}`, function () { + if (test.description === SKIP) { + this.test.skip(); + } executeUriValidationTest(test); }); } diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index edc27e44d57..bc234cd4fa0 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -125,12 +125,14 @@ const EXPECTED_EXPORTS = [ 'ServerType', 'SrvPollingEvent', 'Timestamp', + 'TokenCache', 'TopologyClosedEvent', 'TopologyDescriptionChangedEvent', 'TopologyOpeningEvent', 'TopologyType', 'UnorderedBulkOperation', 'UUID', + 'Workflow', 'WriteConcern', 'ServerSelectionEvent', 'ServerSelectionFailedEvent', From 043620d7e6055253a0af15db4ade8c80e58a0c8d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 20:59:02 +0200 Subject: [PATCH 33/64] test: fix index test --- test/unit/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index bc234cd4fa0..8f8f60aa619 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -96,6 +96,7 @@ const EXPECTED_EXPORTS = [ 'MongoNetworkError', 'MongoNetworkTimeoutError', 'MongoNotConnectedError', + 'MongoOIDCError', 'MongoParseError', 'MongoRuntimeError', 'MongoServerClosedError', From c740bf223debca0185af455aa270ba1be0e8539e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 21:56:16 +0200 Subject: [PATCH 34/64] test: fix gcp unified testS --- test/tools/runner/hooks/configuration.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 1db57745eef..9736995f8fb 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -140,6 +140,7 @@ const testConfigBeforeHook = async function () { .command({ getParameter: '*' }) .catch(error => ({ noReply: error })); + console.log(process.env); this.configuration = new TestConfiguration( loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, context From 89f2ecca7b2f03914502bd929246e458abef952c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 7 May 2024 22:12:47 +0200 Subject: [PATCH 35/64] test: hold off on gcp unified for local env --- .evergreen/config.in.yml | 12 ------------ .evergreen/config.yml | 12 ------------ test/tools/runner/hooks/configuration.js | 1 - 3 files changed, 25 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 3d1c5fbd79c..19a94262a22 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1277,18 +1277,6 @@ tasks: SCRIPT: run-oidc-prose-tests.sh args: - .evergreen/run-oidc-tests-gcp.sh - - command: subprocess.exec - type: test - params: - working_dir: src - binary: bash - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - ENVIRONMENT: gcp - SCRIPT: run-oidc-unified-tests.sh - args: - - .evergreen/run-oidc-tests-gcp.sh - name: "test-aws-lambda-deployed" commands: diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 782de1779ef..155c3af7bed 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1229,18 +1229,6 @@ tasks: SCRIPT: run-oidc-prose-tests.sh args: - .evergreen/run-oidc-tests-gcp.sh - - command: subprocess.exec - type: test - params: - working_dir: src - binary: bash - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - ENVIRONMENT: gcp - SCRIPT: run-oidc-unified-tests.sh - args: - - .evergreen/run-oidc-tests-gcp.sh - name: test-aws-lambda-deployed commands: - command: expansions.update diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 9736995f8fb..1db57745eef 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -140,7 +140,6 @@ const testConfigBeforeHook = async function () { .command({ getParameter: '*' }) .catch(error => ({ noReply: error })); - console.log(process.env); this.configuration = new TestConfiguration( loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, context From 1ae6cc194dbe8a50a223ea1790488f0d67ada5e9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 8 May 2024 20:22:47 +0200 Subject: [PATCH 36/64] test: fix fail points --- test/tools/unified-spec-runner/entities.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/tools/unified-spec-runner/entities.ts b/test/tools/unified-spec-runner/entities.ts index 654a1c9af64..5d09d0874b2 100644 --- a/test/tools/unified-spec-runner/entities.ts +++ b/test/tools/unified-spec-runner/entities.ts @@ -369,7 +369,7 @@ export class FailPointMap extends Map { } else { // create a new client address = addressOrClient.toString(); - client = getClient(address, this.isSrv); + client = getClient(address, false); try { await client.connect(); } catch (error) { @@ -398,7 +398,7 @@ export class FailPointMap extends Map { if (process.env.SERVERLESS || process.env.LOAD_BALANCER) { hostAddress += '?loadBalanced=true'; } - const client = getClient(hostAddress, this.isSrv); + const client = getClient(hostAddress, false); try { await client.connect(); } catch (error) { From f3098abccfdec77fed83470b772c1ccd7c8bd367 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 8 May 2024 22:08:52 +0200 Subject: [PATCH 37/64] test: use oidc uri in config --- test/tools/runner/hooks/configuration.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 1db57745eef..57ca74bc4f0 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -33,6 +33,8 @@ const MONGODB_API_VERSION = process.env.MONGODB_API_VERSION; const SINGLE_MONGOS_LB_URI = process.env.SINGLE_MONGOS_LB_URI; // Load balancer fronting 2 mongoses. const MULTI_MONGOS_LB_URI = process.env.MULTI_MONGOS_LB_URI; +// OIDC scripts all set MONGODB_URI_SINGLE +const OIDC_URI = process.env.MONGODB_URI_SINGLE; const loadBalanced = SINGLE_MONGOS_LB_URI && MULTI_MONGOS_LB_URI; const filters = []; @@ -114,12 +116,21 @@ const testConfigBeforeHook = async function () { return; } - const client = new MongoClient(loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, { + const options = { ...getEnvironmentalOptions(), // TODO(NODE-4884): once happy eyeballs support is added, we no longer need to set // the default dns resolution order for CI family: 4 - }); + }; + if (OIDC_URI && process.env.ENVIRONMENT) { + options.authMechanismProperties = { + ENVIRONMENT: process.env.ENVIRONMENT + }; + } + const client = new MongoClient( + loadBalanced ? SINGLE_MONGOS_LB_URI : OIDC_URI ? OIDC_URI : MONGODB_URI, + options + ); await client.db('test').command({ ping: 1 }); From 6b81a1d5b3265f796a32b57ebd095f009000713a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 10 May 2024 15:40:20 +0200 Subject: [PATCH 38/64] Revert "test: use oidc uri in config" This reverts commit 67c931c154234fa1a20f5bc0cfcd64bfa870d71d. --- test/tools/runner/hooks/configuration.js | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 57ca74bc4f0..1db57745eef 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -33,8 +33,6 @@ const MONGODB_API_VERSION = process.env.MONGODB_API_VERSION; const SINGLE_MONGOS_LB_URI = process.env.SINGLE_MONGOS_LB_URI; // Load balancer fronting 2 mongoses. const MULTI_MONGOS_LB_URI = process.env.MULTI_MONGOS_LB_URI; -// OIDC scripts all set MONGODB_URI_SINGLE -const OIDC_URI = process.env.MONGODB_URI_SINGLE; const loadBalanced = SINGLE_MONGOS_LB_URI && MULTI_MONGOS_LB_URI; const filters = []; @@ -116,21 +114,12 @@ const testConfigBeforeHook = async function () { return; } - const options = { + const client = new MongoClient(loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, { ...getEnvironmentalOptions(), // TODO(NODE-4884): once happy eyeballs support is added, we no longer need to set // the default dns resolution order for CI family: 4 - }; - if (OIDC_URI && process.env.ENVIRONMENT) { - options.authMechanismProperties = { - ENVIRONMENT: process.env.ENVIRONMENT - }; - } - const client = new MongoClient( - loadBalanced ? SINGLE_MONGOS_LB_URI : OIDC_URI ? OIDC_URI : MONGODB_URI, - options - ); + }); await client.db('test').command({ ping: 1 }); From b0fdfb7094da1791397df9ff8aba0789856b7f13 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 10 May 2024 16:06:16 +0200 Subject: [PATCH 39/64] test: use oidc config --- test/tools/runner/config.ts | 15 +++++++++++++++ test/tools/runner/hooks/configuration.js | 10 +++++++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index a27790b207d..9ca2f00338a 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -408,3 +408,18 @@ export class AstrolabeTestConfiguration extends TestConfiguration { return process.env.DRIVERS_ATLAS_TESTING_URI!; } } + +/** + * Test configuration specific to OIDC testing. + */ +export class OIDCTestConfiguration extends TestConfiguration { + override newClient(): MongoClient { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return new MongoClient(process.env.MONGODB_URI_SINGLE!); + } + + override url(): string { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return process.env.MONGODB_URI_SINGLE!; + } +} diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 1db57745eef..054c3f1654e 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -7,7 +7,11 @@ require('source-map-support').install({ const path = require('path'); const fs = require('fs'); const { MongoClient } = require('../../../mongodb'); -const { AstrolabeTestConfiguration, TestConfiguration } = require('../config'); +const { + AstrolabeTestConfiguration, + OIDCTestConfiguration, + TestConfiguration +} = require('../config'); const { getEnvironmentalOptions } = require('../../utils'); const mock = require('../../mongodb-mock/index'); const { inspect } = require('util'); @@ -114,6 +118,10 @@ const testConfigBeforeHook = async function () { return; } + if (process.env.MONGODB_URI_SINGLE) { + this.configuration = new OIDCTestConfiguration(process.env.MONGODB_URI_SINGLE, {}); + return; + } const client = new MongoClient(loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, { ...getEnvironmentalOptions(), // TODO(NODE-4884): once happy eyeballs support is added, we no longer need to set From dc1609664004f94f7e91646db087cc8e7680fd9f Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 12 May 2024 19:45:37 +0200 Subject: [PATCH 40/64] Revert "test: use oidc config" This reverts commit 2a2b3b3d1b09c27f0a5825acd793df20abae7afc. --- test/tools/runner/config.ts | 15 --------------- test/tools/runner/hooks/configuration.js | 10 +--------- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index 9ca2f00338a..a27790b207d 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -408,18 +408,3 @@ export class AstrolabeTestConfiguration extends TestConfiguration { return process.env.DRIVERS_ATLAS_TESTING_URI!; } } - -/** - * Test configuration specific to OIDC testing. - */ -export class OIDCTestConfiguration extends TestConfiguration { - override newClient(): MongoClient { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return new MongoClient(process.env.MONGODB_URI_SINGLE!); - } - - override url(): string { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - return process.env.MONGODB_URI_SINGLE!; - } -} diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 054c3f1654e..1db57745eef 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -7,11 +7,7 @@ require('source-map-support').install({ const path = require('path'); const fs = require('fs'); const { MongoClient } = require('../../../mongodb'); -const { - AstrolabeTestConfiguration, - OIDCTestConfiguration, - TestConfiguration -} = require('../config'); +const { AstrolabeTestConfiguration, TestConfiguration } = require('../config'); const { getEnvironmentalOptions } = require('../../utils'); const mock = require('../../mongodb-mock/index'); const { inspect } = require('util'); @@ -118,10 +114,6 @@ const testConfigBeforeHook = async function () { return; } - if (process.env.MONGODB_URI_SINGLE) { - this.configuration = new OIDCTestConfiguration(process.env.MONGODB_URI_SINGLE, {}); - return; - } const client = new MongoClient(loadBalanced ? SINGLE_MONGOS_LB_URI : MONGODB_URI, { ...getEnvironmentalOptions(), // TODO(NODE-4884): once happy eyeballs support is added, we no longer need to set From 94a9fb93cce640468b27d35e299d49b9c0d333d6 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 12 May 2024 20:27:18 +0200 Subject: [PATCH 41/64] test: use oidc test env config --- .evergreen/config.in.yml | 2 ++ .evergreen/config.yml | 2 ++ test/integration/auth/mongodb_oidc.prose.test.ts | 3 ++- test/tools/runner/hooks/configuration.js | 6 +++++- 4 files changed, 11 insertions(+), 2 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 19a94262a22..eab1c6fbdea 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1438,6 +1438,8 @@ task_groups: params: binary: bash include_expansions_in_env: ["AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] + env: + MONGODB_VERSION: "8.0" args: - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh setup_group_can_fail_task: true diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 155c3af7bed..e60beb8d5b1 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -4462,6 +4462,8 @@ task_groups: - AWS_ACCESS_KEY_ID - AWS_SECRET_ACCESS_KEY - AWS_SESSION_TOKEN + env: + MONGODB_VERSION: '8.0' args: - ${DRIVERS_TOOLS}/.evergreen/auth_oidc/setup.sh setup_group_can_fail_task: true diff --git a/test/integration/auth/mongodb_oidc.prose.test.ts b/test/integration/auth/mongodb_oidc.prose.test.ts index 9f61597f527..69b7cc0c44a 100644 --- a/test/integration/auth/mongodb_oidc.prose.test.ts +++ b/test/integration/auth/mongodb_oidc.prose.test.ts @@ -56,12 +56,13 @@ describe('OIDC Auth Spec Tests', function () { }); describe('1.1 Callback is called during authentication', function () { - const callbackSpy = sinon.spy(createCallback()); + const callbackSpy = sinon.spy(createCallback('test_machine')); // Create an OIDC configured client. // Perform a find operation that succeeds. // Assert that the callback was called 1 time. // Close the client. beforeEach(function () { + console.log(process.env); client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 1db57745eef..3ae9a788615 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -7,7 +7,11 @@ require('source-map-support').install({ const path = require('path'); const fs = require('fs'); const { MongoClient } = require('../../../mongodb'); -const { AstrolabeTestConfiguration, TestConfiguration } = require('../config'); +const { + AstrolabeTestConfiguration, + TestConfiguration, + OIDCTestConfiguration +} = require('../config'); const { getEnvironmentalOptions } = require('../../utils'); const mock = require('../../mongodb-mock/index'); const { inspect } = require('util'); From 17f88c75c1c2903dd4046e56578b850f10f3096a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 12 May 2024 22:08:19 +0200 Subject: [PATCH 42/64] fix: lint --- test/tools/runner/hooks/configuration.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 3ae9a788615..1db57745eef 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -7,11 +7,7 @@ require('source-map-support').install({ const path = require('path'); const fs = require('fs'); const { MongoClient } = require('../../../mongodb'); -const { - AstrolabeTestConfiguration, - TestConfiguration, - OIDCTestConfiguration -} = require('../config'); +const { AstrolabeTestConfiguration, TestConfiguration } = require('../config'); const { getEnvironmentalOptions } = require('../../utils'); const mock = require('../../mongodb-mock/index'); const { inspect } = require('util'); From 0fbca12b61f173aea02244d75ff82f6fabd29086 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 14 May 2024 16:48:33 +0200 Subject: [PATCH 43/64] fix: comments --- src/client-side-encryption/providers/azure.ts | 5 ++-- src/cmap/auth/mongo_credentials.ts | 3 +-- src/index.ts | 4 +-- .../auth/mongodb_oidc.prose.test.ts | 1 - ...ion.prose.18.azure_kms_mock_server.test.ts | 4 +-- test/tools/unified-spec-runner/entities.ts | 26 +++++++------------ test/tools/uri_spec_runner.ts | 4 +-- test/unit/index.test.ts | 2 -- 8 files changed, 19 insertions(+), 30 deletions(-) diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index 1daf5852106..b62283694a4 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -1,6 +1,7 @@ import { type Document } from '../../bson'; +import { MongoNetworkTimeoutError } from '../../error'; import { get } from '../../utils'; -import { MongoCryptAzureKMSRequestError, MongoCryptKMSRequestNetworkTimeoutError } from '../errors'; +import { MongoCryptAzureKMSRequestError } from '../errors'; import { type KMSProviders } from './index'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; @@ -152,7 +153,7 @@ export async function fetchAzureKMSToken( const response = await get(url, { headers }); return await parseResponse(response); } catch (error) { - if (error instanceof MongoCryptKMSRequestNetworkTimeoutError) { + if (error instanceof MongoNetworkTimeoutError) { throw new MongoCryptAzureKMSRequestError(`[Azure KMS] ${error.message}`); } throw error; diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index aa89c56c590..1b5800b1a81 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -3,7 +3,6 @@ import type { Document } from '../../bson'; import { MongoAPIError, - MongoAzureError, MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; @@ -212,7 +211,7 @@ export class MongoCredentials { this.mechanismProperties.ENVIRONMENT === 'gcp') && !this.mechanismProperties.TOKEN_RESOURCE ) { - throw new MongoAzureError(TOKEN_RESOURCE_MISSING_ERROR); + throw new MongoInvalidArgumentError(TOKEN_RESOURCE_MISSING_ERROR); } if ( diff --git a/src/index.ts b/src/index.ts index 8213e31f88f..7c0bfdf841d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -258,8 +258,8 @@ export type { OIDCCallbackParams, OIDCResponse } from './cmap/auth/mongodb_oidc'; -export { Workflow } from './cmap/auth/mongodb_oidc'; -export { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; +export type { Workflow } from './cmap/auth/mongodb_oidc'; +export type { TokenCache } from './cmap/auth/mongodb_oidc/token_cache'; export type { MessageHeader, OpCompressedRequest, diff --git a/test/integration/auth/mongodb_oidc.prose.test.ts b/test/integration/auth/mongodb_oidc.prose.test.ts index 69b7cc0c44a..62e79db6d1b 100644 --- a/test/integration/auth/mongodb_oidc.prose.test.ts +++ b/test/integration/auth/mongodb_oidc.prose.test.ts @@ -62,7 +62,6 @@ describe('OIDC Auth Spec Tests', function () { // Assert that the callback was called 1 time. // Close the client. beforeEach(function () { - console.log(process.env); client = new MongoClient(uriSingle, { authMechanismProperties: { OIDC_CALLBACK: callbackSpy diff --git a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts index 5f092b0f111..c99820b6f83 100644 --- a/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts +++ b/test/integration/client-side-encryption/client_side_encryption.prose.18.azure_kms_mock_server.test.ts @@ -7,7 +7,7 @@ import { type AzureKMSRequestOptions, fetchAzureKMSToken } from '../../../src/client-side-encryption/providers/azure'; -import { type Document, MongoNetworkTimeoutError } from '../../mongodb'; +import { type Document } from '../../mongodb'; const BASE_URL = new URL(`http://127.0.0.1:8080/metadata/identity/oauth2/token`); class KMSRequestOptions implements AzureKMSRequestOptions { @@ -119,7 +119,7 @@ context('Azure KMS Mock Server Tests', function () { it('returns an error after the request times out', async () => { const error = await fetchAzureKMSToken(new KMSRequestOptions('slow')).catch(e => e); - expect(error).to.be.instanceof(MongoNetworkTimeoutError); + expect(error).to.be.instanceof(MongoCryptAzureKMSRequestError); }); }); }); diff --git a/test/tools/unified-spec-runner/entities.ts b/test/tools/unified-spec-runner/entities.ts index 5d09d0874b2..3289a2932d2 100644 --- a/test/tools/unified-spec-runner/entities.ts +++ b/test/tools/unified-spec-runner/entities.ts @@ -118,8 +118,8 @@ export type SdamEvent = | ServerClosedEvent; export type LogMessage = Omit; -function getClient(address, isSrv?: boolean) { - return new MongoClient(`mongodb${isSrv ? '+srv' : ''}://${address}`, getEnvironmentalOptions()); +function getClient(address) { + return new MongoClient(`mongodb://${address}`, getEnvironmentalOptions()); } export class UnifiedMongoClient extends MongoClient { @@ -350,11 +350,8 @@ export class UnifiedMongoClient extends MongoClient { } export class FailPointMap extends Map { - isSrv: boolean; - - constructor(isSrv: boolean) { + constructor() { super(); - this.isSrv = isSrv; } async enableFailPoint( @@ -369,7 +366,7 @@ export class FailPointMap extends Map { } else { // create a new client address = addressOrClient.toString(); - client = getClient(address, false); + client = getClient(address); try { await client.connect(); } catch (error) { @@ -398,7 +395,7 @@ export class FailPointMap extends Map { if (process.env.SERVERLESS || process.env.LOAD_BALANCER) { hostAddress += '?loadBalanced=true'; } - const client = getClient(hostAddress, false); + const client = getClient(hostAddress); try { await client.connect(); } catch (error) { @@ -469,12 +466,10 @@ const NO_INSTANCE_CHECK = ['errors', 'failures', 'events', 'successes', 'iterati export class EntitiesMap extends Map { failPoints: FailPointMap; - isSrv: boolean; - constructor(isSrv: boolean, entries?: readonly (readonly [string, E])[] | null) { + constructor(entries?: readonly (readonly [string, E])[] | null) { super(entries); - this.isSrv = isSrv; - this.failPoints = new FailPointMap(isSrv); + this.failPoints = new FailPointMap(); } mapOf(type: 'client'): EntitiesMap; @@ -490,10 +485,7 @@ export class EntitiesMap extends Map { if (!ctor) { throw new Error(`Unknown type ${type}`); } - return new EntitiesMap( - this.isSrv, - Array.from(this.entries()).filter(([, e]) => e instanceof ctor) - ); + return new EntitiesMap(Array.from(this.entries()).filter(([, e]) => e instanceof ctor)); } getChangeStreamOrCursor(key: string): UnifiedChangeStream | AbstractCursor { @@ -573,7 +565,7 @@ export class EntitiesMap extends Map { entities?: EntityDescription[], entityMap?: EntitiesMap ): Promise { - const map = entityMap ?? new EntitiesMap(config.isSrv); + const map = entityMap ?? new EntitiesMap(); for (const entity of entities ?? []) { if ('client' in entity) { const useMultipleMongoses = diff --git a/test/tools/uri_spec_runner.ts b/test/tools/uri_spec_runner.ts index 17ae4d65786..8502fff3c44 100644 --- a/test/tools/uri_spec_runner.ts +++ b/test/tools/uri_spec_runner.ts @@ -2,8 +2,8 @@ import { expect } from 'chai'; import { MongoAPIError, - MongoAzureError, MongoClient, + MongoInvalidArgumentError, MongoParseError, MongoRuntimeError } from '../mongodb'; @@ -75,7 +75,7 @@ export function executeUriValidationTest( new MongoClient(test.uri); expect.fail(`Expected "${test.uri}" to be invalid${test.valid ? ' because of warning' : ''}`); } catch (err) { - if (err instanceof MongoAzureError) { + if (err instanceof MongoInvalidArgumentError) { // Azure URI errors don't have an underlying cause. } else if (err instanceof MongoRuntimeError) { expect(err).to.have.nested.property('cause.code').equal('ERR_INVALID_URL'); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 8f8f60aa619..6509568c018 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -126,14 +126,12 @@ const EXPECTED_EXPORTS = [ 'ServerType', 'SrvPollingEvent', 'Timestamp', - 'TokenCache', 'TopologyClosedEvent', 'TopologyDescriptionChangedEvent', 'TopologyOpeningEvent', 'TopologyType', 'UnorderedBulkOperation', 'UUID', - 'Workflow', 'WriteConcern', 'ServerSelectionEvent', 'ServerSelectionFailedEvent', From 36f755cfbace0361b4884b0209aed9ca6f7646b2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 14 May 2024 17:36:21 +0200 Subject: [PATCH 44/64] test: fix unit test imports --- .../auth/mongodb_oidc/azure_machine_workflow.test.ts | 9 +++------ .../cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts | 4 +++- .../auth/mongodb_oidc/token_machine_workflow.test.ts | 9 +++------ 3 files changed, 9 insertions(+), 13 deletions(-) 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 index fddc1181d82..b60c4f045da 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -1,12 +1,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { - AzureMachineWorkflow, - Connection, - MongoCredentials, - TokenCache -} from '../../../../mongodb'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; +import { AzureMachineWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; describe('AzureMachineFlow', function () { describe('#execute', function () { 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 index b401f96152e..4cdd2bb4b2e 100644 --- a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -1,7 +1,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { Connection, GCPMachineWorkflow, MongoCredentials, TokenCache } from '../../../../mongodb'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; +import { Connection, GCPMachineWorkflow, MongoCredentials } from '../../../../mongodb'; describe('GCPMachineFlow', function () { describe('#execute', function () { diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts index 3507213dbfc..b0302d7f03e 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -1,12 +1,9 @@ import { expect } from 'chai'; import * as sinon from 'sinon'; -import { - Connection, - MongoCredentials, - TokenCache, - TokenMachineWorkflow -} from '../../../../mongodb'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; +import { Connection, MongoCredentials, TokenMachineWorkflow } from '../../../../mongodb'; describe('TokenMachineFlow', function () { describe('#execute', function () { From 158d9fe3d114697ea1d176cda5ad0b6592fb1eba Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 14 May 2024 17:55:26 +0200 Subject: [PATCH 45/64] test: fix stub --- .../providers/credentialsProvider.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index 03343c398bb..115023e3392 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -23,6 +23,7 @@ import { AWSSDKCredentialProvider } from '../../../../src/cmap/auth/aws_temporar // eslint-disable-next-line @typescript-eslint/no-restricted-imports import * as utils from '../../../../src/utils'; import * as requirements from '../requirements.helper'; +import { MongoNetworkTimeoutError } from '../../../mongodb'; const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; @@ -437,9 +438,7 @@ describe('#refreshKMSCredentials', function () { afterEach(() => sinon.restore()); context('when the request times out', () => { before(() => { - sinon - .stub(utils, 'get') - .rejects(new MongoCryptKMSRequestNetworkTimeoutError('request timed out')); + sinon.stub(utils, 'get').rejects(new MongoNetworkTimeoutError('request timed out')); }); it('throws a MongoCryptKMSRequestError', async () => { From ae14727bbbc224878a90fedbddca4a73dfc0b293 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 14 May 2024 18:04:59 +0200 Subject: [PATCH 46/64] fix: lint --- .../providers/credentialsProvider.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index 115023e3392..f7d5adae940 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -3,10 +3,7 @@ import * as http from 'http'; import * as sinon from 'sinon'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { - MongoCryptAzureKMSRequestError, - MongoCryptKMSRequestNetworkTimeoutError -} from '../../../../src/client-side-encryption/errors'; +import { MongoCryptAzureKMSRequestError } from '../../../../src/client-side-encryption/errors'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { isEmptyCredentials, @@ -22,8 +19,8 @@ import { import { AWSSDKCredentialProvider } from '../../../../src/cmap/auth/aws_temporary_credentials'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import * as utils from '../../../../src/utils'; -import * as requirements from '../requirements.helper'; import { MongoNetworkTimeoutError } from '../../../mongodb'; +import * as requirements from '../requirements.helper'; const originalAccessKeyId = process.env.AWS_ACCESS_KEY_ID; const originalSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY; From 00ef40a816852fdef5fde788b585c1d8c396059c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 15 May 2024 17:31:16 +0200 Subject: [PATCH 47/64] test: add todo --- test/unit/assorted/auth.spec.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/assorted/auth.spec.test.ts b/test/unit/assorted/auth.spec.test.ts index 7553851c972..d67aae5e3f8 100644 --- a/test/unit/assorted/auth.spec.test.ts +++ b/test/unit/assorted/auth.spec.test.ts @@ -1,6 +1,7 @@ import { loadSpecTests } from '../../spec'; import { executeUriValidationTest } from '../../tools/uri_spec_runner'; +// TODO(NODE-6172): Handle commas in TOKEN_RESOURCE. const SKIP = 'should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)'; describe('Auth option spec tests (legacy)', function () { From fe2f415cf4da035fed1fdaa3b1792df405471f5e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 27 May 2024 16:01:18 -0400 Subject: [PATCH 48/64] fix: everything but debounce --- src/client-side-encryption/providers/azure.ts | 24 +++++++++++++------ .../mongodb_oidc/azure_machine_workflow.ts | 18 +++++++------- .../auth/mongodb_oidc/callback_workflow.ts | 2 +- .../auth/mongodb_oidc/machine_workflow.ts | 7 +++--- src/error.ts | 4 ++-- 5 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index b62283694a4..4c06be1893a 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -5,6 +5,8 @@ import { MongoCryptAzureKMSRequestError } from '../errors'; import { type KMSProviders } from './index'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; +/** Base URL for getting Azure tokens. */ +const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?'; /** * The access token that libmongocrypt expects for Azure kms. @@ -114,6 +116,20 @@ export interface AzureKMSRequestOptions { url?: URL | string; } +/** + * @internal + * Get the Azure endpoint URL. + */ +export function getAzureURL(resource: string, username?: string): URL { + const url = new URL(AZURE_BASE_URL); + url.searchParams.append('api-version', '2018-02-01'); + url.searchParams.append('resource', resource); + if (username) { + url.searchParams.append('client_id', username); + } + return url; +} + /** * @internal * @@ -124,13 +140,7 @@ export function prepareRequest(options: AzureKMSRequestOptions): { headers: Document; url: URL; } { - const url = new URL( - options.url?.toString() ?? 'http://169.254.169.254/metadata/identity/oauth2/token' - ); - - url.searchParams.append('api-version', '2018-02-01'); - url.searchParams.append('resource', 'https://vault.azure.net'); - + const url = getAzureURL('https://vault.azure.net'); const headers = { ...options.headers, 'Content-Type': 'application/json', Metadata: true }; return { headers, url }; } diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 40df03fc055..504e51b258e 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,12 +1,10 @@ +import { getAzureURL } from '../../../client-side-encryption/providers/azure'; import { MongoAzureError } from '../../../error'; import { get } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; import { type AccessToken, MachineWorkflow } from './machine_workflow'; import { type TokenCache } from './token_cache'; -/** Base URL for getting Azure tokens. */ -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' }); @@ -52,12 +50,7 @@ export class AzureMachineWorkflow extends MachineWorkflow { * Hit the Azure endpoint to get the token data. */ 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('client_id', username); - } + const url = getAzureURL(tokenAudience, username); const response = await get(url, { headers: AZURE_HEADERS }); @@ -78,5 +71,10 @@ function isEndpointResultValid( token: unknown ): token is { access_token: unknown; expires_in: unknown } { if (token == null || typeof token !== 'object') return false; - return 'access_token' in token && 'expires_in' in token; + return ( + 'access_token' in token && + typeof token.access_token === 'string' && + 'expires_in' in token && + typeof token.expires_in === 'number' + ); } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index a8e23234efe..9b46994e51b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -127,7 +127,7 @@ export abstract class CallbackWorkflow implements Workflow { /** * Ensure the callback is only executed one at a time. */ - protected withLock(callback: OIDCCallbackFunction) { + protected withLock(callback: OIDCCallbackFunction): OIDCCallbackFunction { let lock: Promise = Promise.resolve(); return async (params: OIDCCallbackParams): Promise => { // We do this to ensure that we would never return the result of the diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index d131fe0bc03..e19f53590f5 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -84,12 +84,13 @@ export abstract class MachineWorkflow implements Workflow { /** * Ensure the callback is only executed one at a time. */ - private withLock(callback: OIDCTokenFunction) { + private withLock(callback: OIDCTokenFunction): OIDCTokenFunction { let lock: Promise = Promise.resolve(); return async (credentials: MongoCredentials): Promise => { + // We do this to ensure that we would never return the result of the + // previous lock, only the current callback's value would get returned. await lock; - // eslint-disable-next-line github/no-then - lock = lock.then(() => callback(credentials)); + lock = callback(credentials); return await lock; }; } diff --git a/src/error.ts b/src/error.ts index f198fe88b5a..294062e3d1c 100644 --- a/src/error.ts +++ b/src/error.ts @@ -565,7 +565,7 @@ export class MongoOIDCError extends MongoRuntimeError { * @public * @category Error */ -export class MongoAzureError extends MongoRuntimeError { +export class MongoAzureError extends MongoOIDCError { /** * **Do not use this constructor!** * @@ -593,7 +593,7 @@ export class MongoAzureError extends MongoRuntimeError { * @public * @category Error */ -export class MongoGCPError extends MongoRuntimeError { +export class MongoGCPError extends MongoOIDCError { /** * **Do not use this constructor!** * From d30ccd2e0582427c02804e4e56f54a0e52044896 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 27 May 2024 19:41:19 -0400 Subject: [PATCH 49/64] feat: add workflow executor --- .../auth/mongodb_oidc/callback_workflow.ts | 2 + .../auth/mongodb_oidc/workflow_executor.ts | 43 ++++++++ .../mongodb_oidc/workflow_executor.test.ts | 104 ++++++++++++++++++ 3 files changed, 149 insertions(+) create mode 100644 src/cmap/auth/mongodb_oidc/workflow_executor.ts create mode 100644 test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 9b46994e51b..8146728e4b5 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -25,6 +25,8 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; const CALLBACK_RESULT_ERROR = 'User provided OIDC callbacks must return a valid object with an accessToken.'; +export type RequestAccessTokenFunction = (credentials: MongoCredentials) => Promise; + /** * OIDC implementation of a callback based workflow. * @internal diff --git a/src/cmap/auth/mongodb_oidc/workflow_executor.ts b/src/cmap/auth/mongodb_oidc/workflow_executor.ts new file mode 100644 index 00000000000..8fc5e4e865a --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/workflow_executor.ts @@ -0,0 +1,43 @@ +import { MongoOIDCError } from '../../../error'; +import type { MongoCredentials } from '../mongo_credentials'; +import { type OIDCResponse } from '../mongodb_oidc'; +import type { RequestAccessTokenFunction } from './callback_workflow'; + +/* The no response error message. */ +const NO_RESPONSE = 'No OIDC response found even though the workflow has been executed.'; + +/** + * Executes workflow functions that return OIDC responses, throttling/debouncing + * for the provided debounceMS time. + * @internal + */ +export class WorkflowExecutor { + debounceMS: number; + lastExecutionTime: number; + oidcResponse?: OIDCResponse; + + constructor(debounceMS: number) { + this.debounceMS = debounceMS; + this.lastExecutionTime = Date.now() - debounceMS; + } + + /** + * Execute the function. + */ + async execute( + fn: RequestAccessTokenFunction, + credentials: MongoCredentials + ): Promise { + // If we have passed debounceMS since the last execution time, execute the + // function, set the last execution time, and set the last execution value. + if (Date.now() - this.lastExecutionTime > this.debounceMS) { + this.oidcResponse = await fn(credentials); + this.lastExecutionTime = Date.now(); + } + // If there's no response and we haven't thrown already, throw now. + if (!this.oidcResponse) { + throw new MongoOIDCError(NO_RESPONSE); + } + return this.oidcResponse; + } +} diff --git a/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts b/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts new file mode 100644 index 00000000000..dd60b5310cb --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts @@ -0,0 +1,104 @@ +import { expect } from 'chai'; +import * as sinon from 'sinon'; + +import type { OIDCResponse } from '../../../../../mongodb'; +// eslint-disable-next-line @typescript-eslint/no-restricted-imports +import { WorkflowExecutor } from '../../../../../src/cmap/auth/mongodb_oidc/workflow_executor'; +import { MongoCredentials } from '../../../../mongodb'; + +describe('WorkflowExecutor', function () { + const credentials = sinon.createStubInstance(MongoCredentials); + const fn = async (_credentials: MongoCredentials): Promise => { + return { accessToken: 'test' }; + }; + let clock; + + beforeEach(function () { + clock = sinon.useFakeTimers(Date.now()); + }); + + afterEach(function () { + clock.restore(); + }); + + context('when executing for the first time', function () { + context('when a response is returned', function () { + let result; + const executor = new WorkflowExecutor(100); + + beforeEach(async function () { + result = await executor.execute(fn, credentials); + }); + + it('returns the response', function () { + expect(result.accessToken).to.equal('test'); + }); + + it('sets the last response on the executor', function () { + expect(executor.oidcResponse).to.deep.equal({ accessToken: 'test' }); + }); + + it('sets the last execution time on the executor', function () { + expect(executor.lastExecutionTime).to.be.lessThan(Date.now()); + }); + }); + + context('when a response is not returned', function () { + const fnNull = async (_credentials: MongoCredentials): Promise => { + return null; + }; + const executor = new WorkflowExecutor(100); + + it('throws an error', async function () { + const error = await executor.execute(fnNull, credentials).catch(error => error); + expect(error.message).to.include('No OIDC response'); + }); + }); + }); + + context('when not executing for the first time', function () { + const fnTwo = async (_credentials: MongoCredentials): Promise => { + return { accessToken: 'test2' }; + }; + + context('when the debounce time has not passed', function () { + let result; + let lastExecutionTime; + const executor = new WorkflowExecutor(100); + + beforeEach(async function () { + await executor.execute(fn, credentials); + lastExecutionTime = executor.lastExecutionTime; + clock.tick(50); + result = await executor.execute(fnTwo, credentials); + }); + + it('returns the last response', function () { + expect(result.accessToken).to.equal('test'); + }); + + it('keeps the last execution time on the executor', function () { + expect(executor.lastExecutionTime).to.equal(lastExecutionTime); + }); + }); + + context('when the debounce time has passed', function () { + let result; + const executor = new WorkflowExecutor(100); + + beforeEach(async function () { + await executor.execute(fn, credentials); + clock.tick(150); + result = await executor.execute(fnTwo, credentials); + }); + + it('returns the next response', function () { + expect(result.accessToken).to.equal('test2'); + }); + + it('sets the last execution time on the executor', function () { + expect(executor.lastExecutionTime).to.be.lessThan(Date.now()); + }); + }); + }); +}); From d4a9275550b79b9fff414ad701e03d64da6d367d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 27 May 2024 19:50:36 -0400 Subject: [PATCH 50/64] fix: use workflow executor --- .../automated_callback_workflow.ts | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index cbaa14a56ce..f7ef0e69090 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -1,5 +1,3 @@ -import { setTimeout } from 'timers/promises'; - import { MONGODB_ERROR_CODES, MongoError, MongoOIDCError } from '../../../error'; import { Timeout, TimeoutError } from '../../../timeout'; import { type Connection } from '../../connection'; @@ -12,6 +10,7 @@ import { } from '../mongodb_oidc'; import { AUTOMATED_TIMEOUT_MS, CallbackWorkflow } from './callback_workflow'; import { type TokenCache } from './token_cache'; +import { WorkflowExecutor } from './workflow_executor'; /** Must wait at least 100ms between invocations */ const CALLBACK_DEBOUNCE_MS = 100; @@ -21,14 +20,14 @@ const CALLBACK_DEBOUNCE_MS = 100; * @internal */ export class AutomatedCallbackWorkflow extends CallbackWorkflow { - private lastInvocationTime: number; + private workflowExecutor: WorkflowExecutor; /** * Instantiate the human callback workflow. */ constructor(cache: TokenCache, callback: OIDCCallbackFunction) { super(cache, callback); - this.lastInvocationTime = Date.now(); + this.workflowExecutor = new WorkflowExecutor(CALLBACK_DEBOUNCE_MS); } /** @@ -67,19 +66,10 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } } } - let response: OIDCResponse; - const now = Date.now(); - if (now - this.lastInvocationTime > CALLBACK_DEBOUNCE_MS) { - response = await this.fetchAccessToken(credentials); - } else { - // Ensure a delay between invokations to not overload the callback. - const responses = await Promise.all([ - setTimeout(CALLBACK_DEBOUNCE_MS - (now - this.lastInvocationTime)), - this.fetchAccessToken(credentials) - ]); - response = responses[1]; - } - this.lastInvocationTime = now; + const response = await this.workflowExecutor.execute( + this.fetchAccessToken.bind(this), + credentials + ); this.cache.put(response); await this.finishAuthentication(connection, credentials, response.accessToken); } From 86e1323fa4e8183cf3cd6544cec1af9d0051c2c9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 27 May 2024 23:00:45 -0400 Subject: [PATCH 51/64] test: no more custom uri --- .../providers/credentialsProvider.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts index f7d5adae940..a21ac96ef33 100644 --- a/test/unit/client-side-encryption/providers/credentialsProvider.test.ts +++ b/test/unit/client-side-encryption/providers/credentialsProvider.test.ts @@ -411,18 +411,6 @@ describe('#refreshKMSCredentials', function () { }); }); - it('allows a custom URL to be specified', () => { - const url = httpSpy.args[0][0]; - expect(url).to.be.instanceof(URL); - expect(url.toString()).to.include('http://customentpoint.com'); - }); - - it('deep copies the provided url', () => { - const spiedUrl = httpSpy.args[0][0]; - expect(spiedUrl).to.be.instanceof(URL); - expect(spiedUrl).to.not.equal(url); - }); - it('allows custom headers to be specified', () => { const options = httpSpy.args[0][1]; expect(options).to.have.property('headers').to.have.property('customHeader1', 'value1'); From 15a37d9ac8cb6434ce6c934f589c34afe8b8dbca Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 09:51:49 -0400 Subject: [PATCH 52/64] test: fix azure --- src/client-side-encryption/providers/azure.ts | 8 ++++---- src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/client-side-encryption/providers/azure.ts b/src/client-side-encryption/providers/azure.ts index 4c06be1893a..97a2665ee9a 100644 --- a/src/client-side-encryption/providers/azure.ts +++ b/src/client-side-encryption/providers/azure.ts @@ -6,7 +6,7 @@ import { type KMSProviders } from './index'; const MINIMUM_TOKEN_REFRESH_IN_MILLISECONDS = 6000; /** Base URL for getting Azure tokens. */ -const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?'; +export const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?'; /** * The access token that libmongocrypt expects for Azure kms. @@ -120,8 +120,7 @@ export interface AzureKMSRequestOptions { * @internal * Get the Azure endpoint URL. */ -export function getAzureURL(resource: string, username?: string): URL { - const url = new URL(AZURE_BASE_URL); +export function addAzureParams(url: URL, resource: string, username?: string): URL { url.searchParams.append('api-version', '2018-02-01'); url.searchParams.append('resource', resource); if (username) { @@ -140,7 +139,8 @@ export function prepareRequest(options: AzureKMSRequestOptions): { headers: Document; url: URL; } { - const url = getAzureURL('https://vault.azure.net'); + const url = new URL(options.url?.toString() ?? AZURE_BASE_URL); + addAzureParams(url, 'https://vault.azure.net'); const headers = { ...options.headers, 'Content-Type': 'application/json', Metadata: true }; return { headers, url }; } diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 504e51b258e..6b50db0a83b 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,4 +1,4 @@ -import { getAzureURL } from '../../../client-side-encryption/providers/azure'; +import { addAzureParams, AZURE_BASE_URL } from '../../../client-side-encryption/providers/azure'; import { MongoAzureError } from '../../../error'; import { get } from '../../../utils'; import type { MongoCredentials } from '../mongo_credentials'; @@ -50,7 +50,8 @@ export class AzureMachineWorkflow extends MachineWorkflow { * Hit the Azure endpoint to get the token data. */ async function getAzureTokenData(tokenAudience: string, username?: string): Promise { - const url = getAzureURL(tokenAudience, username); + const url = new URL(AZURE_BASE_URL); + addAzureParams(url, tokenAudience, username); const response = await get(url, { headers: AZURE_HEADERS }); From 6bb6d486fc2844105312fc47b116c037d4f0fdb5 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 10:34:45 -0400 Subject: [PATCH 53/64] test: remove workflow executor --- .../automated_callback_workflow.ts | 12 +- .../mongodb_oidc/azure_machine_workflow.ts | 6 +- .../auth/mongodb_oidc/callback_workflow.ts | 24 +++- .../auth/mongodb_oidc/workflow_executor.ts | 43 -------- .../mongodb_oidc/workflow_executor.test.ts | 104 ------------------ 5 files changed, 26 insertions(+), 163 deletions(-) delete mode 100644 src/cmap/auth/mongodb_oidc/workflow_executor.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index f7ef0e69090..bf9fabc1a9d 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -10,24 +10,17 @@ import { } from '../mongodb_oidc'; import { AUTOMATED_TIMEOUT_MS, CallbackWorkflow } from './callback_workflow'; import { type TokenCache } from './token_cache'; -import { WorkflowExecutor } from './workflow_executor'; - -/** Must wait at least 100ms between invocations */ -const CALLBACK_DEBOUNCE_MS = 100; /** * Class implementing behaviour for the non human callback workflow. * @internal */ export class AutomatedCallbackWorkflow extends CallbackWorkflow { - private workflowExecutor: WorkflowExecutor; - /** * Instantiate the human callback workflow. */ constructor(cache: TokenCache, callback: OIDCCallbackFunction) { super(cache, callback); - this.workflowExecutor = new WorkflowExecutor(CALLBACK_DEBOUNCE_MS); } /** @@ -66,10 +59,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } } } - const response = await this.workflowExecutor.execute( - this.fetchAccessToken.bind(this), - credentials - ); + const response = await this.fetchAccessToken(credentials); this.cache.put(response); await this.finishAuthentication(connection, credentials, response.accessToken); } diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 6b50db0a83b..1f41b8dc08d 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -60,7 +60,11 @@ async function getAzureTokenData(tokenAudience: string, username?: string): Prom `Status code ${response.status} returned from the Azure endpoint. Response body: ${response.body}` ); } - return JSON.parse(response.body); + const result = JSON.parse(response.body); + return { + access_token: result.access_token, + expires_in: Number(result.expires_in) + }; } /** diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 8146728e4b5..d25dd5dbf15 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,4 +1,5 @@ import { type Document } from 'bson'; +import { setTimeout } from 'timers'; import { MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; @@ -25,7 +26,8 @@ const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; const CALLBACK_RESULT_ERROR = 'User provided OIDC callbacks must return a valid object with an accessToken.'; -export type RequestAccessTokenFunction = (credentials: MongoCredentials) => Promise; +/** The time to throttle callback calls. */ +const THROTTLE_MS = 100; /** * OIDC implementation of a callback based workflow. @@ -34,6 +36,7 @@ export type RequestAccessTokenFunction = (credentials: MongoCredentials) => Prom export abstract class CallbackWorkflow implements Workflow { cache: TokenCache; callback: OIDCCallbackFunction; + lastExecutionTime: number; /** * Instantiate the callback workflow. @@ -41,6 +44,7 @@ export abstract class CallbackWorkflow implements Workflow { constructor(cache: TokenCache, callback: OIDCCallbackFunction) { this.cache = cache; this.callback = this.withLock(callback); + this.lastExecutionTime = Date.now() - THROTTLE_MS; } /** @@ -116,14 +120,26 @@ export abstract class CallbackWorkflow implements Workflow { * Executes the callback and validates the output. */ protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise { - // With no token in the cache we use the request callback. - const result = await this.callback(params); + // With no token in the cache we use the request callback this needs to be throttled + // every 100ms. + let result; + const difference = Date.now() - this.lastExecutionTime; + if (difference > THROTTLE_MS) { + result = await this.callback(params); + } else { + result = await new Promise(resolve => { + setTimeout(() => { + this.lastExecutionTime = Date.now(); + resolve(this.callback(params)); + }, THROTTLE_MS - difference); + }); + } // 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)) { throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } - return result; + return result as OIDCResponse; } /** diff --git a/src/cmap/auth/mongodb_oidc/workflow_executor.ts b/src/cmap/auth/mongodb_oidc/workflow_executor.ts deleted file mode 100644 index 8fc5e4e865a..00000000000 --- a/src/cmap/auth/mongodb_oidc/workflow_executor.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { MongoOIDCError } from '../../../error'; -import type { MongoCredentials } from '../mongo_credentials'; -import { type OIDCResponse } from '../mongodb_oidc'; -import type { RequestAccessTokenFunction } from './callback_workflow'; - -/* The no response error message. */ -const NO_RESPONSE = 'No OIDC response found even though the workflow has been executed.'; - -/** - * Executes workflow functions that return OIDC responses, throttling/debouncing - * for the provided debounceMS time. - * @internal - */ -export class WorkflowExecutor { - debounceMS: number; - lastExecutionTime: number; - oidcResponse?: OIDCResponse; - - constructor(debounceMS: number) { - this.debounceMS = debounceMS; - this.lastExecutionTime = Date.now() - debounceMS; - } - - /** - * Execute the function. - */ - async execute( - fn: RequestAccessTokenFunction, - credentials: MongoCredentials - ): Promise { - // If we have passed debounceMS since the last execution time, execute the - // function, set the last execution time, and set the last execution value. - if (Date.now() - this.lastExecutionTime > this.debounceMS) { - this.oidcResponse = await fn(credentials); - this.lastExecutionTime = Date.now(); - } - // If there's no response and we haven't thrown already, throw now. - if (!this.oidcResponse) { - throw new MongoOIDCError(NO_RESPONSE); - } - return this.oidcResponse; - } -} diff --git a/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts b/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts deleted file mode 100644 index dd60b5310cb..00000000000 --- a/test/unit/cmap/auth/mongodb_oidc/workflow_executor.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { expect } from 'chai'; -import * as sinon from 'sinon'; - -import type { OIDCResponse } from '../../../../../mongodb'; -// eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { WorkflowExecutor } from '../../../../../src/cmap/auth/mongodb_oidc/workflow_executor'; -import { MongoCredentials } from '../../../../mongodb'; - -describe('WorkflowExecutor', function () { - const credentials = sinon.createStubInstance(MongoCredentials); - const fn = async (_credentials: MongoCredentials): Promise => { - return { accessToken: 'test' }; - }; - let clock; - - beforeEach(function () { - clock = sinon.useFakeTimers(Date.now()); - }); - - afterEach(function () { - clock.restore(); - }); - - context('when executing for the first time', function () { - context('when a response is returned', function () { - let result; - const executor = new WorkflowExecutor(100); - - beforeEach(async function () { - result = await executor.execute(fn, credentials); - }); - - it('returns the response', function () { - expect(result.accessToken).to.equal('test'); - }); - - it('sets the last response on the executor', function () { - expect(executor.oidcResponse).to.deep.equal({ accessToken: 'test' }); - }); - - it('sets the last execution time on the executor', function () { - expect(executor.lastExecutionTime).to.be.lessThan(Date.now()); - }); - }); - - context('when a response is not returned', function () { - const fnNull = async (_credentials: MongoCredentials): Promise => { - return null; - }; - const executor = new WorkflowExecutor(100); - - it('throws an error', async function () { - const error = await executor.execute(fnNull, credentials).catch(error => error); - expect(error.message).to.include('No OIDC response'); - }); - }); - }); - - context('when not executing for the first time', function () { - const fnTwo = async (_credentials: MongoCredentials): Promise => { - return { accessToken: 'test2' }; - }; - - context('when the debounce time has not passed', function () { - let result; - let lastExecutionTime; - const executor = new WorkflowExecutor(100); - - beforeEach(async function () { - await executor.execute(fn, credentials); - lastExecutionTime = executor.lastExecutionTime; - clock.tick(50); - result = await executor.execute(fnTwo, credentials); - }); - - it('returns the last response', function () { - expect(result.accessToken).to.equal('test'); - }); - - it('keeps the last execution time on the executor', function () { - expect(executor.lastExecutionTime).to.equal(lastExecutionTime); - }); - }); - - context('when the debounce time has passed', function () { - let result; - const executor = new WorkflowExecutor(100); - - beforeEach(async function () { - await executor.execute(fn, credentials); - clock.tick(150); - result = await executor.execute(fnTwo, credentials); - }); - - it('returns the next response', function () { - expect(result.accessToken).to.equal('test2'); - }); - - it('sets the last execution time on the executor', function () { - expect(executor.lastExecutionTime).to.be.lessThan(Date.now()); - }); - }); - }); -}); From 076db10705e1b311345d035199df973e55222983 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 11:58:45 -0400 Subject: [PATCH 54/64] test: remove console logs --- test/integration/auth/mongodb_oidc.prose.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/integration/auth/mongodb_oidc.prose.test.ts b/test/integration/auth/mongodb_oidc.prose.test.ts index 62e79db6d1b..b2acf89e577 100644 --- a/test/integration/auth/mongodb_oidc.prose.test.ts +++ b/test/integration/auth/mongodb_oidc.prose.test.ts @@ -1026,7 +1026,6 @@ describe('OIDC Auth Spec Tests', function () { retryReads: false }); collection = client.db('test').collection('testHuman'); - console.log('setting fail point'); await utilClient .db() .admin() @@ -1051,7 +1050,6 @@ describe('OIDC Auth Spec Tests', function () { }); it('does not successfully authenticate', async function () { - console.log('execute find'); const error = await collection.findOne().catch(error => error); expect(error).to.exist; }); From 183f485a210539deba99c3c5a23d4a41c42a2352 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 13:59:44 -0400 Subject: [PATCH 55/64] fix: locking --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 3 ++- src/cmap/auth/mongodb_oidc/machine_workflow.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index d25dd5dbf15..4f4994b1571 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -151,7 +151,8 @@ export abstract class CallbackWorkflow implements Workflow { // We do this to ensure that we would never return the result of the // previous lock, only the current callback's value would get returned. await lock; - lock = callback(params); + // eslint-disable-next-line github/no-then + lock = lock.then(() => callback(params)); return await lock; }; } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index e19f53590f5..ed8f7a25113 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -90,7 +90,8 @@ export abstract class MachineWorkflow implements Workflow { // We do this to ensure that we would never return the result of the // previous lock, only the current callback's value would get returned. await lock; - lock = callback(credentials); + // eslint-disable-next-line github/no-then + lock = lock.then(() => callback(credentials)); return await lock; }; } From 2e97a7235be617b15dfa27086370d0dcca9a9beb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 14:46:20 -0400 Subject: [PATCH 56/64] fix: move throttling to withLock --- .../auth/mongodb_oidc/callback_workflow.ts | 36 ++++++++++--------- .../auth/mongodb_oidc/machine_workflow.ts | 25 +++++++++++-- 2 files changed, 42 insertions(+), 19 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 4f4994b1571..12b2fe4844b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -120,30 +120,18 @@ export abstract class CallbackWorkflow implements Workflow { * Executes the callback and validates the output. */ protected async executeAndValidateCallback(params: OIDCCallbackParams): Promise { - // With no token in the cache we use the request callback this needs to be throttled - // every 100ms. - let result; - const difference = Date.now() - this.lastExecutionTime; - if (difference > THROTTLE_MS) { - result = await this.callback(params); - } else { - result = await new Promise(resolve => { - setTimeout(() => { - this.lastExecutionTime = Date.now(); - resolve(this.callback(params)); - }, THROTTLE_MS - difference); - }); - } + const result = await this.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)) { throw new MongoMissingCredentialsError(CALLBACK_RESULT_ERROR); } - return result as OIDCResponse; + return result; } /** - * Ensure the callback is only executed one at a time. + * Ensure the callback is only executed one at a time and throttles the calls + * to every 100ms. */ protected withLock(callback: OIDCCallbackFunction): OIDCCallbackFunction { let lock: Promise = Promise.resolve(); @@ -152,7 +140,21 @@ export abstract class CallbackWorkflow implements Workflow { // previous lock, only the current callback's value would get returned. await lock; // eslint-disable-next-line github/no-then - lock = lock.then(() => callback(params)); + lock = lock.then(async () => { + let result; + const difference = Date.now() - this.lastExecutionTime; + if (difference > THROTTLE_MS) { + result = await callback(params); + } else { + result = await new Promise(resolve => { + setTimeout(() => { + this.lastExecutionTime = Date.now(); + resolve(callback(params)); + }, THROTTLE_MS - difference); + }); + } + return result; + }); return await lock; }; } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index ed8f7a25113..1c3d63edce3 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -1,4 +1,5 @@ import { type Document } from 'bson'; +import { setTimeout } from 'timers'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; @@ -7,6 +8,9 @@ import type { Workflow } from '../mongodb_oidc'; import { finishCommandDocument } from './command_builders'; import { type TokenCache } from './token_cache'; +/** The time to throttle callback calls. */ +const THROTTLE_MS = 100; + /** * The access token format. * @internal @@ -26,6 +30,7 @@ export type OIDCTokenFunction = (credentials: MongoCredentials) => Promise = Promise.resolve(); @@ -91,7 +98,21 @@ export abstract class MachineWorkflow implements Workflow { // previous lock, only the current callback's value would get returned. await lock; // eslint-disable-next-line github/no-then - lock = lock.then(() => callback(credentials)); + lock = lock.then(async () => { + let result; + const difference = Date.now() - this.lastExecutionTime; + if (difference > THROTTLE_MS) { + result = await callback(credentials); + } else { + result = await new Promise(resolve => { + setTimeout(() => { + this.lastExecutionTime = Date.now(); + resolve(callback(credentials)); + }, THROTTLE_MS - difference); + }); + } + return result; + }); return await lock; }; } From 84547b09f93b344aa81d323996d154847daad8de Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 28 May 2024 15:09:18 -0400 Subject: [PATCH 57/64] feat: add connection cache --- src/cmap/auth/mongodb_oidc.ts | 5 ++-- .../automated_callback_workflow.ts | 12 +------- .../auth/mongodb_oidc/callback_workflow.ts | 28 +++++++++++++++--- .../mongodb_oidc/human_callback_workflow.ts | 14 +-------- .../auth/mongodb_oidc/machine_workflow.ts | 29 +++++++++++++++---- src/cmap/connection.ts | 1 + 6 files changed, 53 insertions(+), 36 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index fdfb929f1d4..e44436b5ab9 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -111,7 +111,7 @@ export interface Workflow { /** * Get the document to add for speculative authentication. */ - speculativeAuth(credentials: MongoCredentials): Promise; + speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise; } /** @internal */ @@ -160,8 +160,9 @@ export class MongoDBOIDC extends AuthProvider { handshakeDoc: HandshakeDocument, authContext: AuthContext ): Promise { + const { connection } = authContext; const credentials = getCredentials(authContext); - const result = await this.workflow.speculativeAuth(credentials); + const result = await this.workflow.speculativeAuth(connection, credentials); return { ...handshakeDoc, ...result }; } } diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index bf9fabc1a9d..f98d87f6a27 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -23,17 +23,6 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { super(cache, callback); } - /** - * Reauthenticate the callback workflow. For this we invalidated the access token - * in the cache and run the authentication steps again. No initial handshake needs - * to be sent. - */ - async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { - // Reauthentication should always remove the access token. - this.cache.removeAccessToken(); - await this.execute(connection, credentials); - } - /** * Execute the OIDC callback workflow. */ @@ -61,6 +50,7 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { } const response = await this.fetchAccessToken(credentials); this.cache.put(response); + connection.accessToken = response.accessToken; await this.finishAuthentication(connection, credentials, response.accessToken); } diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 12b2fe4844b..69810c542c2 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -51,12 +51,14 @@ export abstract class CallbackWorkflow implements Workflow { * Get the document to add for speculative authentication. This also needs * to add a db field from the credentials source. */ - async speculativeAuth(credentials: MongoCredentials): Promise { + async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise { // Check if the Client Cache has an access token. // If it does, cache the access token in the Connection Cache and send a JwtStepRequest // with the cached access token in the speculative authentication SASL payload. if (this.cache.hasAccessToken) { - const document = finishCommandDocument(this.cache.getAccessToken()); + const accessToken = this.cache.getAccessToken(); + connection.accessToken = accessToken; + const document = finishCommandDocument(accessToken); document.db = credentials.source; return { speculativeAuthenticate: document }; } @@ -64,9 +66,27 @@ export abstract class CallbackWorkflow implements Workflow { } /** - * Each workflow should specify the correct custom behaviour for reauthentication. + * Reauthenticate the callback workflow. For this we invalidated the access token + * in the cache and run the authentication steps again. No initial handshake needs + * to be sent. */ - abstract reauthenticate(connection: Connection, credentials: MongoCredentials): Promise; + async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { + if (this.cache.hasAccessToken) { + // Reauthentication implies the token has expired. + if (connection.accessToken === this.cache.getAccessToken()) { + // If connection's access token is the same as the cache's, remove + // the token from the cache and connection. + this.cache.removeAccessToken(); + delete connection.accessToken; + } else { + // If the connection's access token is different from the cache's, set + // the cache's token on the connection and do not remove from the + // cache. + connection.accessToken = this.cache.getAccessToken(); + } + } + await this.execute(connection, credentials); + } /** * Execute the OIDC callback workflow. diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index 8de5ad3637b..77f6f086b74 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -26,19 +26,6 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { super(cache, callback); } - /** - * Reauthenticate the callback workflow. For this we invalidated the access token - * in the cache and run the authentication steps again. No initial handshake needs - * to be sent. - */ - async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { - // Reauthentication should always remove the access token, but in the - // human workflow we need to pass the refesh token through if it - // exists. - this.cache.removeAccessToken(); - await this.execute(connection, credentials); - } - /** * Execute the OIDC human callback workflow. */ @@ -106,6 +93,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { const idpInfo = BSON.deserialize(startResponse.payload.buffer) as IdPInfo; const callbackResponse = await this.fetchAccessToken(idpInfo, credentials); this.cache.put(callbackResponse, idpInfo); + connection.accessToken = callbackResponse.accessToken; return await this.finishAuthentication( connection, credentials, diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index 1c3d63edce3..cfadf40d1e6 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -45,7 +45,7 @@ export abstract class MachineWorkflow implements Workflow { * Execute the workflow. Gets the token from the subclass implementation. */ async execute(connection: Connection, credentials: MongoCredentials): Promise { - const token = await this.getTokenFromCacheOrEnv(credentials); + const token = await this.getTokenFromCacheOrEnv(connection, credentials); const command = finishCommandDocument(token); await connection.command(ns(credentials.source), command, undefined); } @@ -55,20 +55,32 @@ export abstract class MachineWorkflow implements Workflow { * has said the current access token is invalid or expired. */ async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { - // Reauthentication implies the token has expired. - this.cache.removeAccessToken(); + if (this.cache.hasAccessToken) { + // Reauthentication implies the token has expired. + if (connection.accessToken === this.cache.getAccessToken()) { + // If connection's access token is the same as the cache's, remove + // the token from the cache and connection. + this.cache.removeAccessToken(); + delete connection.accessToken; + } else { + // If the connection's access token is different from the cache's, set + // the cache's token on the connection and do not remove from the + // cache. + connection.accessToken = this.cache.getAccessToken(); + } + } await this.execute(connection, credentials); } /** * Get the document to add for speculative authentication. */ - async speculativeAuth(credentials: MongoCredentials): Promise { + async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise { // The spec states only cached access tokens can use speculative auth. if (!this.cache.hasAccessToken) { return {}; } - const token = await this.getTokenFromCacheOrEnv(credentials); + const token = await this.getTokenFromCacheOrEnv(connection, credentials); const document = finishCommandDocument(token); document.db = credentials.source; return { speculativeAuthenticate: document }; @@ -77,12 +89,17 @@ export abstract class MachineWorkflow implements Workflow { /** * Get the token from the cache or environment. */ - private async getTokenFromCacheOrEnv(credentials: MongoCredentials): Promise { + private async getTokenFromCacheOrEnv( + connection: Connection, + credentials: MongoCredentials + ): Promise { if (this.cache.hasAccessToken) { return this.cache.getAccessToken(); } else { const token = await this.callback(credentials); this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); + // Put the access token on the connection as well. + connection.accessToken = token.access_token; return token.access_token; } } diff --git a/src/cmap/connection.ts b/src/cmap/connection.ts index e1ad9a02935..c6420d8306e 100644 --- a/src/cmap/connection.ts +++ b/src/cmap/connection.ts @@ -174,6 +174,7 @@ export class Connection extends TypedEventEmitter { public authContext?: AuthContext; public delayedTimeoutId: NodeJS.Timeout | null = null; public generation: number; + public accessToken?: string; public readonly description: Readonly; /** * Represents if the connection has been established: From 8cb6c064f20520a7c61a49313dec920b74ab4695 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 09:51:56 -0400 Subject: [PATCH 58/64] test: update for DRIVERS-2915 --- test/spec/auth/legacy/connection-string.json | 15 --------------- test/spec/auth/legacy/connection-string.yml | 11 ----------- test/unit/assorted/auth.spec.test.ts | 6 ------ 3 files changed, 32 deletions(-) diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index f4c7f8c88ea..84c2dbc6684 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -599,21 +599,6 @@ } } }, - { - "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", - "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abc%2Cd%25ef%3Ag%26hi", - "valid": true, - "credential": { - "username": "user", - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "ENVIRONMENT": "azure", - "TOKEN_RESOURCE": "abc,d%ef:g&hi" - } - } - }, { "description": "should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)", "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b", diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index c88eb1edce8..65dc4586f4f 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -434,17 +434,6 @@ tests: mechanism_properties: ENVIRONMENT: azure TOKEN_RESOURCE: 'mongodb://test-cluster' -- description: should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC) - uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abc%2Cd%25ef%3Ag%26hi - valid: true - credential: - username: user - password: null - source: $external - mechanism: MONGODB-OIDC - mechanism_properties: - ENVIRONMENT: azure - TOKEN_RESOURCE: 'abc,d%ef:g&hi' - description: should url-encode a TOKEN_RESOURCE (MONGODB-OIDC) uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b valid: true diff --git a/test/unit/assorted/auth.spec.test.ts b/test/unit/assorted/auth.spec.test.ts index d67aae5e3f8..c474fd8cf11 100644 --- a/test/unit/assorted/auth.spec.test.ts +++ b/test/unit/assorted/auth.spec.test.ts @@ -1,9 +1,6 @@ import { loadSpecTests } from '../../spec'; import { executeUriValidationTest } from '../../tools/uri_spec_runner'; -// TODO(NODE-6172): Handle commas in TOKEN_RESOURCE. -const SKIP = 'should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)'; - describe('Auth option spec tests (legacy)', function () { const suites = loadSpecTests('auth', 'legacy'); @@ -11,9 +8,6 @@ describe('Auth option spec tests (legacy)', function () { describe(suite.name, function () { for (const test of suite.tests) { it(`${test.description}`, function () { - if (test.description === SKIP) { - this.test.skip(); - } executeUriValidationTest(test); }); } From 7a97e6bf99a1f7cd2b36a0bcf47ad8928365925c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 09:53:57 -0400 Subject: [PATCH 59/64] docs: doc token resource comma --- src/connection_string.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/connection_string.ts b/src/connection_string.ts index c2abf08aaac..b0becafac05 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -698,6 +698,9 @@ export const OPTIONS = { }); } }, + // Note that if the authMechanismProperties contain a TOKEN_RESOURCE that has a + // comma in it, it MUST be supplied as a MongoClient option instead of in the + // connection string. authMechanismProperties: { target: 'credentials', transform({ options, values }): MongoCredentials { From 5799f7203cca425731c512ab59617e7c1dff7332 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 10:20:46 -0400 Subject: [PATCH 60/64] test: update connection string test --- test/spec/auth/legacy/connection-string.json | 15 +++++++++++++++ test/spec/auth/legacy/connection-string.yml | 11 +++++++++++ 2 files changed, 26 insertions(+) diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index 84c2dbc6684..1c859c332e2 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -599,6 +599,21 @@ } } }, + { + "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", + "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi", + "valid": false, + "credential": { + "username": "user", + "password": null, + "source": "$external", + "mechanism": "MONGODB-OIDC", + "mechanism_properties": { + "ENVIRONMENT": "azure", + "TOKEN_RESOURCE": "abcd%ef:g&hi" + } + } + }, { "description": "should url-encode a TOKEN_RESOURCE (MONGODB-OIDC)", "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b", diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index 65dc4586f4f..a9651133959 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -434,6 +434,17 @@ tests: mechanism_properties: ENVIRONMENT: azure TOKEN_RESOURCE: 'mongodb://test-cluster' +- description: should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC) + uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi + valid: true + credential: + username: user + password: null + source: $external + mechanism: MONGODB-OIDC + mechanism_properties: + ENVIRONMENT: azure + TOKEN_RESOURCE: 'abcd%ef:g&hi' - description: should url-encode a TOKEN_RESOURCE (MONGODB-OIDC) uri: mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:a$b valid: true From 4913053ecaf1e08a8b63b8f7549a87d204bccd31 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 10:47:34 -0400 Subject: [PATCH 61/64] test: fix spec test json --- src/cmap/auth/mongo_credentials.ts | 7 ------- test/spec/auth/legacy/connection-string.json | 2 +- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 1b5800b1a81..3438886eff6 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -135,13 +135,6 @@ export class MongoCredentials { }; } - if (this.mechanism === AuthMechanism.MONGODB_OIDC && this.mechanismProperties.TOKEN_RESOURCE) { - this.mechanismProperties = { - ...this.mechanismProperties, - TOKEN_RESOURCE: decodeURIComponent(this.mechanismProperties.TOKEN_RESOURCE) - }; - } - Object.freeze(this.mechanismProperties); Object.freeze(this); } diff --git a/test/spec/auth/legacy/connection-string.json b/test/spec/auth/legacy/connection-string.json index 1c859c332e2..5b54e2aadd2 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -602,7 +602,7 @@ { "description": "should handle a complicated url-encoded TOKEN_RESOURCE (MONGODB-OIDC)", "uri": "mongodb://user@localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=ENVIRONMENT:azure,TOKEN_RESOURCE:abcd%25ef%3Ag%26hi", - "valid": false, + "valid": true, "credential": { "username": "user", "password": null, From 961a1eeb21a185ee06082b34c2c22b79770205b2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 12:28:27 -0400 Subject: [PATCH 62/64] fix: connection cache in access/refresh --- src/cmap/auth/mongodb_oidc/human_callback_workflow.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts index 77f6f086b74..13ac81a6be5 100644 --- a/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/human_callback_workflow.ts @@ -37,6 +37,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { // and restart the authentication flow. Raise any other errors to the user. On success, exit the algorithm. if (this.cache.hasAccessToken) { const token = this.cache.getAccessToken(); + connection.accessToken = token; try { return await this.finishAuthentication(connection, credentials, token); } catch (error) { @@ -45,6 +46,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { error.code === MONGODB_ERROR_CODES.AuthenticationFailed ) { this.cache.removeAccessToken(); + delete connection.accessToken; return await this.execute(connection, credentials); } else { throw error; @@ -66,6 +68,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { refreshToken ); this.cache.put(result); + connection.accessToken = result.accessToken; try { return await this.finishAuthentication(connection, credentials, result.accessToken); } catch (error) { @@ -74,6 +77,7 @@ export class HumanCallbackWorkflow extends CallbackWorkflow { error.code === MONGODB_ERROR_CODES.AuthenticationFailed ) { this.cache.removeRefreshToken(); + delete connection.accessToken; return await this.execute(connection, credentials); } else { throw error; From b959f13e4515335a12a91005ec7705b347a43d4b Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 13:25:40 -0400 Subject: [PATCH 63/64] fix: dont permanently reject --- .../auth/mongodb_oidc/callback_workflow.ts | 30 ++++++++----------- .../auth/mongodb_oidc/machine_workflow.ts | 30 ++++++++----------- 2 files changed, 26 insertions(+), 34 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 69810c542c2..77a6ceb1c3f 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -1,5 +1,5 @@ import { type Document } from 'bson'; -import { setTimeout } from 'timers'; +import { setTimeout } from 'timers/promises'; import { MongoMissingCredentialsError } from '../../../error'; import { ns } from '../../../utils'; @@ -159,22 +159,18 @@ export abstract class CallbackWorkflow implements Workflow { // We do this to ensure that we would never return the result of the // previous lock, only the current callback's value would get returned. await lock; - // eslint-disable-next-line github/no-then - lock = lock.then(async () => { - let result; - const difference = Date.now() - this.lastExecutionTime; - if (difference > THROTTLE_MS) { - result = await callback(params); - } else { - result = await new Promise(resolve => { - setTimeout(() => { - this.lastExecutionTime = Date.now(); - resolve(callback(params)); - }, THROTTLE_MS - difference); - }); - } - return result; - }); + lock = lock + // eslint-disable-next-line github/no-then + .catch(() => null) + // eslint-disable-next-line github/no-then + .then(async () => { + const difference = Date.now() - this.lastExecutionTime; + if (difference <= THROTTLE_MS) { + await setTimeout(THROTTLE_MS - difference); + } + this.lastExecutionTime = Date.now(); + return await callback(params); + }); return await lock; }; } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts index cfadf40d1e6..b7cbc8ab2e1 100644 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/machine_workflow.ts @@ -1,5 +1,5 @@ import { type Document } from 'bson'; -import { setTimeout } from 'timers'; +import { setTimeout } from 'timers/promises'; import { ns } from '../../../utils'; import type { Connection } from '../../connection'; @@ -114,22 +114,18 @@ export abstract class MachineWorkflow implements Workflow { // We do this to ensure that we would never return the result of the // previous lock, only the current callback's value would get returned. await lock; - // eslint-disable-next-line github/no-then - lock = lock.then(async () => { - let result; - const difference = Date.now() - this.lastExecutionTime; - if (difference > THROTTLE_MS) { - result = await callback(credentials); - } else { - result = await new Promise(resolve => { - setTimeout(() => { - this.lastExecutionTime = Date.now(); - resolve(callback(credentials)); - }, THROTTLE_MS - difference); - }); - } - return result; - }); + lock = lock + // eslint-disable-next-line github/no-then + .catch(() => null) + // eslint-disable-next-line github/no-then + .then(async () => { + const difference = Date.now() - this.lastExecutionTime; + if (difference <= THROTTLE_MS) { + await setTimeout(THROTTLE_MS - difference); + } + this.lastExecutionTime = Date.now(); + return await callback(credentials); + }); return await lock; }; } From e6ce764bdca29bcfe7e04b0d4bd8f6dd88d45d31 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Wed, 29 May 2024 13:33:23 -0400 Subject: [PATCH 64/64] feat: add timeout context --- src/cmap/auth/mongodb_oidc/callback_workflow.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 77a6ceb1c3f..4f273367f2b 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -166,7 +166,7 @@ export abstract class CallbackWorkflow implements Workflow { .then(async () => { const difference = Date.now() - this.lastExecutionTime; if (difference <= THROTTLE_MS) { - await setTimeout(THROTTLE_MS - difference); + await setTimeout(THROTTLE_MS - difference, { signal: params.timeoutContext }); } this.lastExecutionTime = Date.now(); return await callback(params);