From 6b6e892b3fefa5e542382f74c0248709c7e352fb Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 26 Jan 2024 15:35:31 +0100 Subject: [PATCH] feat(NODE-5815): remove oidc --- .evergreen/config.in.yml | 100 -- .evergreen/config.yml | 125 -- .evergreen/generate_evergreen_tasks.js | 28 +- .evergreen/run-oidc-tests-azure.sh | 11 - .evergreen/run-oidc-tests.sh | 35 - src/cmap/auth/mongo_credentials.ts | 98 +- src/cmap/auth/mongodb_oidc.ts | 149 -- .../auth/mongodb_oidc/aws_service_workflow.ts | 29 - .../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 | 114 -- .../auth/mongodb_oidc/callback_workflow.ts | 296 ---- .../auth/mongodb_oidc/service_workflow.ts | 49 - .../auth/mongodb_oidc/token_entry_cache.ts | 77 -- src/cmap/auth/providers.ts | 5 +- src/cmap/connect.ts | 2 - src/connection_string.ts | 3 +- src/error.ts | 17 - src/index.ts | 8 - src/mongo_client.ts | 35 +- src/utils.ts | 17 - .../auth/mongodb_oidc_azure.prose.test.ts | 209 --- test/manual/mongodb_oidc.prose.test.ts | 1230 ----------------- test/mongodb.ts | 8 - test/spec/auth/legacy/connection-string.json | 127 -- test/spec/auth/legacy/connection-string.yml | 104 -- test/unit/cmap/auth/mongodb_oidc.test.ts | 51 - .../mongodb_oidc/aws_service_workflow.test.ts | 34 - .../mongodb_oidc/azure_token_cache.test.ts | 77 -- .../mongodb_oidc/callback_lock_cache.test.ts | 145 -- .../mongodb_oidc/token_entry_cache.test.ts | 144 -- test/unit/connection_string.test.ts | 90 +- test/unit/index.test.ts | 1 - test/unit/utils.test.ts | 143 -- 35 files changed, 9 insertions(+), 3752 deletions(-) delete mode 100644 .evergreen/run-oidc-tests-azure.sh delete mode 100755 .evergreen/run-oidc-tests.sh delete mode 100644 src/cmap/auth/mongodb_oidc.ts delete mode 100644 src/cmap/auth/mongodb_oidc/aws_service_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 delete mode 100644 src/cmap/auth/mongodb_oidc/callback_workflow.ts delete mode 100644 src/cmap/auth/mongodb_oidc/service_workflow.ts delete mode 100644 src/cmap/auth/mongodb_oidc/token_entry_cache.ts delete mode 100644 test/integration/auth/mongodb_oidc_azure.prose.test.ts delete mode 100644 test/manual/mongodb_oidc.prose.test.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc.test.ts delete mode 100644 test/unit/cmap/auth/mongodb_oidc/aws_service_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 delete mode 100644 test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index c3a57e78471..74d35cb0499 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -132,58 +132,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 @@ -1254,26 +1202,6 @@ tasks: args: - 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: - working_dir: src - binary: bash - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} - PROVIDER_NAME: azure - args: - - .evergreen/run-oidc-tests-azure.sh - - name: "test-aws-lambda-deployed" commands: - command: expansions.update @@ -1400,34 +1328,6 @@ task_groups: tasks: - test-azurekms-task - - name: testazureoidc_task_group - setup_group: - - func: fetch source - - command: shell.exec - params: - shell: bash - 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: - - command: shell.exec - params: - shell: bash - script: |- - ${PREPARE_SHELL} - $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh - setup_group_can_fail_task: true - setup_group_timeout_secs: 1800 - tasks: - - oidc-auth-test-azure-latest - - name: test_atlas_task_group setup_group: - func: fetch source diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 1edec2d48bb..bb7ef6a80ec 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -104,55 +104,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 @@ -1205,25 +1156,6 @@ tasks: EXPECTED_AZUREKMS_OUTCOME: failure args: - 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: - working_dir: src - binary: bash - env: - DRIVERS_TOOLS: ${DRIVERS_TOOLS} - PROJECT_DIRECTORY: ${PROJECT_DIRECTORY} - AZUREOIDC_CLIENTID: ${testazureoidc_clientid} - PROVIDER_NAME: azure - args: - - .evergreen/run-oidc-tests-azure.sh - name: test-aws-lambda-deployed commands: - command: expansions.update @@ -1794,25 +1726,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: @@ -3735,33 +3648,6 @@ task_groups: - ${DRIVERS_TOOLS}/.evergreen/csfle/azurekms/delete-vm.sh tasks: - test-azurekms-task - - name: testazureoidc_task_group - setup_group: - - func: fetch source - - command: shell.exec - params: - shell: bash - 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: - - command: shell.exec - params: - shell: bash - script: |- - ${PREPARE_SHELL} - $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/delete-vm.sh - setup_group_can_fail_task: true - setup_group_timeout_secs: 1800 - tasks: - - oidc-auth-test-azure-latest - name: test_atlas_task_group setup_group: - func: fetch source @@ -3846,7 +3732,6 @@ buildvariants: - test-6.0-load-balanced - test-latest-load-balanced - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -3898,7 +3783,6 @@ buildvariants: - test-6.0-load-balanced - test-latest-load-balanced - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -3948,7 +3832,6 @@ buildvariants: - test-6.0-load-balanced - test-latest-load-balanced - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -3998,7 +3881,6 @@ buildvariants: - test-6.0-load-balanced - test-latest-load-balanced - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4047,7 +3929,6 @@ buildvariants: - test-6.0-load-balanced - test-latest-load-balanced - test-auth-ldap - - test-auth-oidc - test-socks5 - test-socks5-csfle - test-socks5-tls @@ -4331,12 +4212,6 @@ buildvariants: tasks: - test_azurekms_task_group - test-azurekms-fail-task - - name: ubuntu20-test-azure-oidc - display_name: Azure OIDC - run_on: ubuntu2004-small - batchtime: 20160 - tasks: - - testazureoidc_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 cd313fad3ee..b022d935d94 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -44,8 +44,7 @@ const WINDOWS_SKIP_TAGS = new Set([ 'atlas-connect', 'auth', 'load_balancer', - 'socks5-csfle', - 'oidc' + 'socks5-csfle' ]); const TASKS = []; @@ -188,23 +187,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: [], @@ -701,14 +683,6 @@ BUILD_VARIANTS.push({ tasks: ['test_azurekms_task_group', 'test-azurekms-fail-task'] }); -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: 'rhel8-test-atlas', display_name: 'Atlas Cluster Tests', diff --git a/.evergreen/run-oidc-tests-azure.sh b/.evergreen/run-oidc-tests-azure.sh deleted file mode 100644 index 6e65bff3f44..00000000000 --- a/.evergreen/run-oidc-tests-azure.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/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 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 PROJECT_DIRECTORY=$PROJECT_DIRECTORY -export PROVIDER_NAME=$PROVIDER_NAME -bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh \ No newline at end of file diff --git a/.evergreen/run-oidc-tests.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/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 150a0841680..b7b8763f161 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -1,14 +1,8 @@ // Resolves the default auth mechanism according to // Resolves the default auth mechanism according to import type { Document } from '../../bson'; -import { - MongoAPIError, - MongoAzureError, - MongoInvalidArgumentError, - MongoMissingCredentialsError -} from '../../error'; +import { MongoAPIError, MongoMissingCredentialsError } from '../../error'; import { GSSAPICanonicalizationValue } from './gssapi'; -import type { OIDCRefreshFunction, OIDCRequestFunction } from './mongodb_oidc'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism } from './providers'; // https://github.com/mongodb/specifications/blob/master/source/auth/auth.rst @@ -32,23 +26,6 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism { return AuthMechanism.MONGODB_CR; } -const ALLOWED_PROVIDER_NAMES: AuthMechanismProperties['PROVIDER_NAME'][] = ['aws', 'azure']; -const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'; - -/** @internal */ -export const DEFAULT_ALLOWED_HOSTS = [ - '*.mongodb.net', - '*.mongodb-dev.net', - '*.mongodbgov.net', - 'localhost', - '127.0.0.1', - '::1' -]; - -/** 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.'; - /** @public */ export interface AuthMechanismProperties extends Document { SERVICE_HOST?: string; @@ -56,16 +33,6 @@ export interface AuthMechanismProperties extends Document { SERVICE_REALM?: string; CANONICALIZE_HOST_NAME?: GSSAPICanonicalizationValue; AWS_SESSION_TOKEN?: string; - /** @experimental */ - REQUEST_TOKEN_CALLBACK?: OIDCRequestFunction; - /** @experimental */ - REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction; - /** @experimental */ - PROVIDER_NAME?: 'aws' | 'azure'; - /** @experimental */ - ALLOWED_HOSTS?: string[]; - /** @experimental */ - TOKEN_AUDIENCE?: string; } /** @public */ @@ -124,13 +91,6 @@ export class MongoCredentials { } } - if (this.mechanism === AuthMechanism.MONGODB_OIDC && !this.mechanismProperties.ALLOWED_HOSTS) { - this.mechanismProperties = { - ...this.mechanismProperties, - ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS - }; - } - Object.freeze(this.mechanismProperties); Object.freeze(this); } @@ -178,62 +138,6 @@ export class MongoCredentials { throw new MongoMissingCredentialsError(`Username required for mechanism '${this.mechanism}'`); } - if (this.mechanism === AuthMechanism.MONGODB_OIDC) { - if (this.username && this.mechanismProperties.PROVIDER_NAME) { - throw new MongoInvalidArgumentError( - `username and PROVIDER_NAME may not be used together for mechanism '${this.mechanism}'.` - ); - } - - if ( - this.mechanismProperties.PROVIDER_NAME === 'azure' && - !this.mechanismProperties.TOKEN_AUDIENCE - ) { - throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); - } - - if ( - this.mechanismProperties.PROVIDER_NAME && - !ALLOWED_PROVIDER_NAMES.includes(this.mechanismProperties.PROVIDER_NAME) - ) { - throw new MongoInvalidArgumentError( - `Currently only a PROVIDER_NAME in ${ALLOWED_PROVIDER_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 - ) { - throw new MongoInvalidArgumentError( - `Either a PROVIDER_NAME or a REQUEST_TOKEN_CALLBACK must be specified for mechanism '${this.mechanism}'.` - ); - } - - if (this.mechanismProperties.ALLOWED_HOSTS) { - const hosts = this.mechanismProperties.ALLOWED_HOSTS; - if (!Array.isArray(hosts)) { - throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); - } - for (const host of hosts) { - if (typeof host !== 'string') { - throw new MongoInvalidArgumentError(ALLOWED_HOSTS_ERROR); - } - } - } - } - if (AUTH_MECHS_AUTH_SRC_EXTERNAL.has(this.mechanism)) { if (this.source != null && this.source !== '$external') { // TODO(NODE-3485): Replace this with a MongoAuthValidationError diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts deleted file mode 100644 index f3584c4893e..00000000000 --- a/src/cmap/auth/mongodb_oidc.ts +++ /dev/null @@ -1,149 +0,0 @@ -import type { Document } from 'bson'; - -import { MongoInvalidArgumentError, 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 { AwsServiceWorkflow } from './mongodb_oidc/aws_service_workflow'; -import { AzureServiceWorkflow } from './mongodb_oidc/azure_service_workflow'; -import { CallbackWorkflow } from './mongodb_oidc/callback_workflow'; - -/** Error when credentials are missing. */ -const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; - -/** - * @public - * @experimental - */ -export interface IdPServerInfo { - issuer: string; - clientId: string; - requestScopes?: string[]; -} - -/** - * @public - * @experimental - */ -export interface IdPServerResponse { - accessToken: string; - expiresInSeconds?: number; - refreshToken?: string; -} - -/** - * @public - * @experimental - */ -export interface OIDCCallbackContext { - refreshToken?: string; - timeoutSeconds?: number; - timeoutContext?: AbortSignal; - version: number; -} - -/** - * @public - * @experimental - */ -export type OIDCRequestFunction = ( - info: IdPServerInfo, - context: OIDCCallbackContext -) => Promise; - -/** - * @public - * @experimental - */ -export type OIDCRefreshFunction = ( - info: IdPServerInfo, - context: OIDCCallbackContext -) => Promise; - -type ProviderName = 'aws' | 'azure' | 'callback'; - -export interface Workflow { - /** - * All device workflows must implement this method in order to get the access - * token and then call authenticate with it. - */ - execute( - connection: Connection, - credentials: MongoCredentials, - reauthenticating: boolean, - response?: Document - ): Promise; - - /** - * Get the document to add for speculative authentication. - */ - speculativeAuth(credentials: MongoCredentials): Promise; -} - -/** @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 auth provider. - * @experimental - */ -export class MongoDBOIDC extends AuthProvider { - /** - * Instantiate the auth provider. - */ - constructor() { - super(); - } - - /** - * Authenticate using OIDC - */ - override async auth(authContext: AuthContext): Promise { - const { connection, reauthenticating, response } = authContext; - const credentials = getCredentials(authContext); - const workflow = getWorkflow(credentials); - await workflow.execute(connection, credentials, reauthenticating, response); - } - - /** - * Add the speculative auth for the initial handshake. - */ - override async prepare( - handshakeDoc: HandshakeDocument, - authContext: AuthContext - ): Promise { - const credentials = getCredentials(authContext); - const workflow = getWorkflow(credentials); - const result = await workflow.speculativeAuth(credentials); - return { ...handshakeDoc, ...result }; - } -} - -/** - * Get credentials from the auth context, throwing if they do not exist. - */ -function getCredentials(authContext: AuthContext): MongoCredentials { - const { credentials } = authContext; - if (!credentials) { - throw new MongoMissingCredentialsError(MISSING_CREDENTIALS_ERROR); - } - return credentials; -} - -/** - * 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'); - if (!workflow) { - throw new MongoInvalidArgumentError( - `Could not load workflow for provider ${credentials.mechanismProperties.PROVIDER_NAME}` - ); - } - return workflow; -} 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 5dd07b1d28e..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 fs.promises.readFile(tokenFile, 'utf8'); - } -} 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 b92a504b0a8..00000000000 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { MongoInvalidArgumentError } from '../../../error'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { - IdPServerInfo, - IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction -} from '../mongodb_oidc'; -import { Cache } from './cache'; - -/** Error message for when request callback is missing. */ -const REQUEST_CALLBACK_REQUIRED_ERROR = - 'Auth mechanism property REQUEST_TOKEN_CALLBACK is required.'; -/* Counter for function "hashes".*/ -let FN_HASH_COUNTER = 0; -/* No function present function */ -const NO_FUNCTION: OIDCRequestFunction = async () => ({ accessToken: 'test' }); -/* The map of function hashes */ -const FN_HASHES = new WeakMap(); -/* Put the no function hash in the map. */ -FN_HASHES.set(NO_FUNCTION, FN_HASH_COUNTER); - -/** - * An entry of callbacks in the cache. - */ -interface CallbacksEntry { - requestCallback: OIDCRequestFunction; - refreshCallback?: OIDCRefreshFunction; - callbackHash: string; -} - -/** - * A cache of request and refresh callbacks per server/user. - */ -export class CallbackLockCache extends Cache { - /** - * Get the callbacks for the connection and credentials. If an entry does not - * exist a new one will get set. - */ - getEntry(connection: Connection, credentials: MongoCredentials): CallbacksEntry { - const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; - const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; - if (!requestCallback) { - throw new MongoInvalidArgumentError(REQUEST_CALLBACK_REQUIRED_ERROR); - } - const callbackHash = hashFunctions(requestCallback, refreshCallback); - const key = this.cacheKey(connection.address, credentials.username, callbackHash); - const entry = this.entries.get(key); - if (entry) { - return entry; - } - return this.addEntry(key, callbackHash, requestCallback, refreshCallback); - } - - /** - * Set locked callbacks on for connection and credentials. - */ - private addEntry( - key: string, - callbackHash: string, - requestCallback: OIDCRequestFunction, - refreshCallback?: OIDCRefreshFunction - ): CallbacksEntry { - const entry = { - requestCallback: withLock(requestCallback), - refreshCallback: refreshCallback ? withLock(refreshCallback) : undefined, - callbackHash: callbackHash - }; - this.entries.set(key, entry); - return entry; - } - - /** - * Create a cache key from the address and username. - */ - cacheKey(address: string, username: string, callbackHash: string): string { - return this.hashedCacheKey(address, username, callbackHash); - } -} - -/** - * Ensure the callback is only executed one at a time. - */ -function withLock(callback: OIDCRequestFunction | OIDCRefreshFunction) { - let lock: Promise = Promise.resolve(); - return async (info: IdPServerInfo, context: OIDCCallbackContext): Promise => { - await lock; - lock = lock.then(() => callback(info, context)); - return lock; - }; -} - -/** - * Get the hash string for the request and refresh functions. - */ -function hashFunctions(requestFn: OIDCRequestFunction, refreshFn?: OIDCRefreshFunction): string { - let requestHash = FN_HASHES.get(requestFn); - let refreshHash = FN_HASHES.get(refreshFn ?? NO_FUNCTION); - if (requestHash == null) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - requestHash = FN_HASH_COUNTER; - FN_HASHES.set(requestFn, FN_HASH_COUNTER); - } - if (refreshHash == null && refreshFn) { - // Create a new one for the function and put it in the map. - FN_HASH_COUNTER++; - refreshHash = FN_HASH_COUNTER; - FN_HASHES.set(refreshFn, FN_HASH_COUNTER); - } - return `${requestHash}-${refreshHash}`; -} diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts deleted file mode 100644 index c220ae5b70c..00000000000 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { Binary, BSON, type Document } from 'bson'; - -import { MONGODB_ERROR_CODES, MongoError, MongoMissingCredentialsError } from '../../../error'; -import { ns } from '../../../utils'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { - IdPServerInfo, - IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction, - Workflow -} from '../mongodb_oidc'; -import { AuthMechanism } from '../providers'; -import { CallbackLockCache } from './callback_lock_cache'; -import { TokenEntryCache } from './token_entry_cache'; - -/** The current version of OIDC implementation. */ -const OIDC_VERSION = 0; - -/** 5 minutes in seconds */ -const TIMEOUT_S = 300; - -/** Properties allowed on results of callbacks. */ -const RESULT_PROPERTIES = ['accessToken', 'expiresInSeconds', 'refreshToken']; - -/** Error message when the callback result is invalid. */ -const CALLBACK_RESULT_ERROR = - 'User provided OIDC callbacks must return a valid object with an accessToken.'; - -/** - * OIDC implementation of a callback based workflow. - * @internal - */ -export class CallbackWorkflow implements Workflow { - cache: TokenEntryCache; - callbackCache: CallbackLockCache; - - /** - * Instantiate the workflow - */ - constructor() { - this.cache = new TokenEntryCache(); - this.callbackCache = new CallbackLockCache(); - } - - /** - * Get the document to add for speculative authentication. This also needs - * to add a db field from the credentials source. - */ - async speculativeAuth(credentials: MongoCredentials): Promise { - const document = startCommandDocument(credentials); - document.db = credentials.source; - return { speculativeAuthenticate: document }; - } - - /** - * Execute the OIDC callback workflow. - */ - async execute( - connection: Connection, - credentials: MongoCredentials, - reauthenticating: boolean, - response?: Document - ): Promise { - // Get the callbacks with locks from the callback lock cache. - const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getEntry( - connection, - credentials - ); - // Look for an existing entry in the cache. - const entry = this.cache.getEntry(connection.address, credentials.username, callbackHash); - let result; - if (entry) { - // Reauthentication cannot use a token from the cache since the server has - // stated it is invalid by the request for reauthentication. - if (entry.isValid() && !reauthenticating) { - // Presence of a valid cache entry means we can skip to the finishing step. - result = await this.finishAuthentication( - connection, - credentials, - entry.tokenResult, - response?.speculativeAuthenticate?.conversationId - ); - } else { - // Presence of an expired cache entry means we must fetch a new one and - // then execute the final step. - const tokenResult = await this.fetchAccessToken( - connection, - credentials, - entry.serverInfo, - reauthenticating, - callbackHash, - requestCallback, - refreshCallback - ); - try { - result = await this.finishAuthentication( - connection, - credentials, - tokenResult, - reauthenticating ? undefined : response?.speculativeAuthenticate?.conversationId - ); - } catch (error) { - // If we are reauthenticating and this errors with reauthentication - // required, we need to do the entire process over again and clear - // the cache entry. - if ( - reauthenticating && - error instanceof MongoError && - error.code === MONGODB_ERROR_CODES.Reauthenticate - ) { - this.cache.deleteEntry(connection.address, credentials.username, callbackHash); - result = await this.execute(connection, credentials, reauthenticating); - } else { - throw error; - } - } - } - } 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; - } - - /** - * 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( - connection: Connection, - credentials: MongoCredentials, - reauthenticating: boolean, - response?: Document - ): Promise { - let result; - if (!reauthenticating && response?.speculativeAuthenticate) { - result = response.speculativeAuthenticate; - } else { - result = await connection.commandAsync( - ns(credentials.source), - startCommandDocument(credentials), - undefined - ); - } - return result; - } - - /** - * Finishes the callback authentication process. - */ - private async finishAuthentication( - connection: Connection, - credentials: MongoCredentials, - tokenResult: IdPServerResponse, - conversationId?: number - ): Promise { - const result = await connection.commandAsync( - ns(credentials.source), - finishCommandDocument(tokenResult.accessToken, conversationId), - undefined - ); - return result; - } - - /** - * Fetches an access token using either the request or refresh callbacks and - * puts it in the cache. - */ - private 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); - } - // 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 })) - }; - } - // 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 })) - }; -} - -/** - * Determines if a result returned from a request or refresh callback - * function is invalid. This means the result is nullish, doesn't contain - * the accessToken required field, and does not contain extra fields. - */ -function isCallbackResultInvalid(tokenResult: unknown): boolean { - if (tokenResult == null || typeof tokenResult !== 'object') return true; - 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/service_workflow.ts b/src/cmap/auth/mongodb_oidc/service_workflow.ts deleted file mode 100644 index fb01e2c24ce..00000000000 --- a/src/cmap/auth/mongodb_oidc/service_workflow.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { BSON, type Document } from 'bson'; - -import { ns } from '../../../utils'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { Workflow } from '../mongodb_oidc'; -import { AuthMechanism } from '../providers'; - -/** - * Common behaviour for OIDC device workflows. - * @internal - */ -export abstract class ServiceWorkflow implements Workflow { - /** - * Execute the workflow. Looks for AWS_WEB_IDENTITY_TOKEN_FILE in the environment - * and then attempts to read the token from that path. - */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { - const token = await this.getToken(credentials); - const command = commandDocument(token); - return connection.commandAsync(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_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/providers.ts b/src/cmap/auth/providers.ts index d01c06324bb..1e4d6f18cf2 100644 --- a/src/cmap/auth/providers.ts +++ b/src/cmap/auth/providers.ts @@ -7,9 +7,7 @@ export const AuthMechanism = Object.freeze({ MONGODB_PLAIN: 'PLAIN', MONGODB_SCRAM_SHA1: 'SCRAM-SHA-1', MONGODB_SCRAM_SHA256: 'SCRAM-SHA-256', - MONGODB_X509: 'MONGODB-X509', - /** @experimental */ - MONGODB_OIDC: 'MONGODB-OIDC' + MONGODB_X509: 'MONGODB-X509' } as const); /** @public */ @@ -19,6 +17,5 @@ export type AuthMechanism = (typeof AuthMechanism)[keyof typeof AuthMechanism]; export const AUTH_MECHS_AUTH_SRC_EXTERNAL = new Set([ AuthMechanism.MONGODB_GSSAPI, AuthMechanism.MONGODB_AWS, - AuthMechanism.MONGODB_OIDC, AuthMechanism.MONGODB_X509 ]); diff --git a/src/cmap/connect.ts b/src/cmap/connect.ts index e191f72ce8d..85bb80e263f 100644 --- a/src/cmap/connect.ts +++ b/src/cmap/connect.ts @@ -21,7 +21,6 @@ import { AuthContext, type AuthProvider } from './auth/auth_provider'; import { GSSAPI } from './auth/gssapi'; import { MongoCR } from './auth/mongocr'; import { MongoDBAWS } from './auth/mongodb_aws'; -import { MongoDBOIDC } from './auth/mongodb_oidc'; import { Plain } from './auth/plain'; import { AuthMechanism } from './auth/providers'; import { ScramSHA1, ScramSHA256 } from './auth/scram'; @@ -45,7 +44,6 @@ export const AUTH_PROVIDERS = new Map([ [AuthMechanism.MONGODB_AWS, new MongoDBAWS()], [AuthMechanism.MONGODB_CR, new MongoCR()], [AuthMechanism.MONGODB_GSSAPI, new GSSAPI()], - [AuthMechanism.MONGODB_OIDC, new MongoDBOIDC()], [AuthMechanism.MONGODB_PLAIN, new Plain()], [AuthMechanism.MONGODB_SCRAM_SHA1, new ScramSHA1()], [AuthMechanism.MONGODB_SCRAM_SHA256, new ScramSHA256()], diff --git a/src/connection_string.ts b/src/connection_string.ts index 0e9d189e696..5a197ad4dea 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -402,7 +402,6 @@ export function parseOptions( const isGssapi = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_GSSAPI; const isX509 = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_X509; const isAws = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_AWS; - const isOidc = mongoOptions.credentials.mechanism === AuthMechanism.MONGODB_OIDC; if ( (isGssapi || isX509) && allProvidedOptions.has('authSource') && @@ -415,7 +414,7 @@ export function parseOptions( } if ( - !(isGssapi || isX509 || isAws || isOidc) && + !(isGssapi || isX509 || isAws) && mongoOptions.dbName && !allProvidedOptions.has('authSource') ) { diff --git a/src/error.ts b/src/error.ts index 8c9f495626d..f7a8d3d7a07 100644 --- a/src/error.ts +++ b/src/error.ts @@ -390,23 +390,6 @@ export class MongoAWSError extends MongoRuntimeError { } } -/** - * A error generated when the user attempts to authenticate - * via Azure, but fails. - * - * @public - * @category Error - */ -export class MongoAzureError extends MongoRuntimeError { - constructor(message: string) { - super(message); - } - - override get name(): string { - return 'MongoAzureError'; - } -} - /** * An error generated when a ChangeStream operation fails to execute. * diff --git a/src/index.ts b/src/index.ts index edcc43610d8..c4960f61402 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,7 +40,6 @@ export { ChangeStreamCursor } from './cursor/change_stream_cursor'; export { MongoAPIError, MongoAWSError, - MongoAzureError, MongoBatchReExecutionError, MongoChangeStreamError, MongoCompatibilityError, @@ -207,13 +206,6 @@ export type { MongoCredentials, MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; -export type { - IdPServerInfo, - IdPServerResponse, - OIDCCallbackContext, - OIDCRefreshFunction, - OIDCRequestFunction -} from './cmap/auth/mongodb_oidc'; export type { BinMsg, MessageHeader, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index f16ec0d4059..270e3fd4cdb 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -4,12 +4,8 @@ import { promisify } from 'util'; import { type BSONSerializeOptions, type Document, resolveBSONOptions } from './bson'; import { ChangeStream, type ChangeStreamDocument, type ChangeStreamOptions } from './change_stream'; -import { - type AuthMechanismProperties, - DEFAULT_ALLOWED_HOSTS, - type MongoCredentials -} from './cmap/auth/mongo_credentials'; -import { AuthMechanism } from './cmap/auth/providers'; +import { type AuthMechanismProperties, type MongoCredentials } from './cmap/auth/mongo_credentials'; +import type { AuthMechanism } from './cmap/auth/providers'; import type { LEGAL_TCP_SOCKET_OPTIONS, LEGAL_TLS_SOCKET_OPTIONS } from './cmap/connect'; import type { Connection } from './cmap/connection'; import type { ClientMetadata } from './cmap/handshake/client_metadata'; @@ -29,13 +25,7 @@ import { readPreferenceServerSelector } from './sdam/server_selection'; import type { SrvPoller } from './sdam/srv_polling'; import { Topology, type TopologyEvents } from './sdam/topology'; import { ClientSession, type ClientSessionOptions, ServerSessionPool } from './sessions'; -import { - type HostAddress, - hostMatchesWildcards, - type MongoDBNamespace, - ns, - resolveOptions -} from './utils'; +import { type HostAddress, type MongoDBNamespace, ns, resolveOptions } from './utils'; import type { W, WriteConcern, WriteConcernSettings } from './write_concern'; /** @public */ @@ -482,25 +472,6 @@ export class MongoClient extends TypedEventEmitter { } } - // It is important to perform validation of hosts AFTER SRV resolution, to check the real hostname, - // but BEFORE we even attempt connecting with a potentially not allowed hostname - if (options.credentials?.mechanism === AuthMechanism.MONGODB_OIDC) { - const allowedHosts = - options.credentials?.mechanismProperties?.ALLOWED_HOSTS || DEFAULT_ALLOWED_HOSTS; - const isServiceAuth = !!options.credentials?.mechanismProperties?.PROVIDER_NAME; - if (!isServiceAuth) { - for (const host of options.hosts) { - if (!hostMatchesWildcards(host.toHostPort().host, allowedHosts)) { - throw new MongoInvalidArgumentError( - `Host '${host}' is not valid for OIDC authentication with ALLOWED_HOSTS of '${allowedHosts.join( - ',' - )}'` - ); - } - } - } - } - this.topology = new Topology(this, options.hosts, options); // Events can be emitted before initialization is complete so we have to // save the reference to the topology on the client ASAP if the event handlers need to access it diff --git a/src/utils.ts b/src/utils.ts index 505f3bfd1d5..2948b0d7293 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -61,23 +61,6 @@ export const ByteUtils = { } }; -/** - * Determines if a connection's address matches a user provided list - * of domain wildcards. - */ -export function hostMatchesWildcards(host: string, wildcards: string[]): boolean { - for (const wildcard of wildcards) { - if ( - host === wildcard || - (wildcard.startsWith('*.') && host?.endsWith(wildcard.substring(2, wildcard.length))) || - (wildcard.startsWith('*/') && host?.endsWith(wildcard.substring(2, wildcard.length))) - ) { - return true; - } - } - return false; -} - /** * Throws if collectionName is not a valid mongodb collection namespace. * @internal diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts deleted file mode 100644 index 2dc95b4c935..00000000000 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { expect } from 'chai'; - -import { - type Collection, - type CommandFailedEvent, - type CommandStartedEvent, - type CommandSucceededEvent, - type MongoClient, - OIDC_WORKFLOWS -} from '../../mongodb'; - -describe('OIDC Auth Spec Prose Tests', function () { - const callbackCache = OIDC_WORKFLOWS.get('callback').cache; - const azureCache = OIDC_WORKFLOWS.get('azure').cache; - - describe('3. Azure Automatic Auth', 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.'; - this.skip(); - } - }); - - afterEach(async function () { - await client?.close(); - }); - - describe('3.1 Connect', function () { - beforeEach(function () { - client = this.configuration.newClient(process.env.MONGODB_URI); - collection = client.db('test').collection('test'); - }); - - // 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. - 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: [] - } - }); - 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; - }); - }); - - 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. - // 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); - 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); - }); - }); - - 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); - } - }; - 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 - } - }); - }; - - // 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(); - }); - - // 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']); - }); - }); - }); -}); diff --git a/test/manual/mongodb_oidc.prose.test.ts b/test/manual/mongodb_oidc.prose.test.ts deleted file mode 100644 index bb4cfcb671f..00000000000 --- a/test/manual/mongodb_oidc.prose.test.ts +++ /dev/null @@ -1,1230 +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, - type IdPServerInfo, - MongoClient, - MongoInvalidArgumentError, - MongoMissingCredentialsError, - MongoServerError, - OIDC_WORKFLOWS, - type OIDCCallbackContext -} from '../mongodb'; -import { sleep } from '../tools/utils'; - -describe('MONGODB-OIDC', 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; - // Creates a request function for use in the test. - const createRequestCallback = ( - 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'); - 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'); - 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 }; - if (expiresInSeconds) { - response.expiresInSeconds = expiresInSeconds; - } - if (extraFields) { - return { ...response, ...extraFields }; - } - 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', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('test'); - }); - - // 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(); - expect(result).to.be.null; - }); - }); - - describe('1.2 Single Principal Explicit Username', function () { - before(function () { - client = new MongoClient('mongodb://test_user1@localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: createRequestCallback() - } - }); - collection = client.db('test').collection('test'); - }); - - // 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 () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - 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() - } - } - ); - collection = client.db('test').collection('test'); - }); - - // 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 () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - 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') - } - } - ); - collection = client.db('test').collection('test'); - }); - - // 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 () { - const result = await collection.findOne(); - expect(result).to.be.null; - }); - }); - - 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() - } - } - ); - collection = client.db('test').collection('test'); - }); - - // 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 () { - 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 () { - 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 () { - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - ALLOWED_HOSTS: [], - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) - } - }); - collection = client.db('test').collection('test'); - }); - - 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'], - // REQUEST_TOKEN_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'], - REQUEST_TOKEN_CALLBACK: createRequestCallback('test_user1', 600) - } - }); - collection = client.db('test').collection('test'); - }); - - 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('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 () { - 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=PROVIDER_NAME:aws' - ); - collection = client.db('test').collection('test'); - }); - - // Create a client with a url of the form mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME: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=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); - }); - - // Create a client with a url of the form mongodb://localhost:27018/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME: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=PROVIDER_NAME:aws&directConnection=true&readPreference=secondaryPreferred' - ); - collection = client.db('test').collection('test'); - }); - - 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=PROVIDER_NAME: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=PROVIDER_NAME:aws', - { - authMechanismProperties: { - ALLOWED_HOSTS: [] - } - } - ); - collection = client.db('test').collection('test'); - }); - - // 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. - // 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 () { - const requestSpy = sinon.spy(createRequestCallback('test_user1', 60)); - const refreshSpy = sinon.spy(createRefreshCallback()); - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestSpy, - REFRESH_TOKEN_CALLBACK: refreshSpy - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - collection = client.db('test').collection('test'); - await collection.findOne(); - expect(requestSpy).to.have.been.calledOnce; - await client.close(); - }); - - // 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. - // 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; - }); - }); - - describe('3.2 Request Callback Returns Null', function () { - before(function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: { - REQUEST_TOKEN_CALLBACK: () => { - return Promise.resolve(null); - } - } - }); - collection = client.db('test').collection('test'); - }); - - // 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 () { - 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.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({}); - } - } - }); - collection = client.db('test').collection('test'); - }); - - // 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 () { - 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' - ); - } - }); - }); - - 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 () { - let client: MongoClient; - const requestCallback = createRequestCallback('test_user1', 600); - const authMechanismProperties = { - REQUEST_TOKEN_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 () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - 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(); - expect(result).to.be.null; - }); - }); - - describe('6. Reauthentication', function () { - let client: MongoClient; - - // Removes the fail point. - const removeFailPoint = async () => { - return await client.db().admin().command({ - configureFailPoint: 'failCommand', - mode: 'off' - }); - }; - - describe('6.1 Succeeds', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const refreshSpy = sinon.spy(createRefreshCallback('test_user1', 600)); - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshSpy - }; - 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 () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - expect(refreshSpy).to.not.be.called; - client.close(); - }); - - afterEach(async function () { - await removeFailPoint(); - 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: - // - // { - // "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 refresh callback has been called once, if possible. - // 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; - 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('6.2 Retries and Succeeds with Cache', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const refreshCallback = createRefreshCallback('test_user1', 600); - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback - }; - // 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', 'saslStart'], - errorCode: 391 - } - }); - }; - - before(async function () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - await setupFailPoint(); - }); - - afterEach(async function () { - await removeFailPoint(); - 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. - // 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 succeeds. - // Close the client. - it('successfully authenticates', async function () { - const result = await client.db('test').collection('test').findOne(); - expect(result).to.be.null; - }); - }); - - describe('6.3 Retries and Fails with no Cache', function () { - const requestCallback = createRequestCallback('test_user1', 600); - const refreshCallback = createRefreshCallback('test_user1', 600); - const authMechanismProperties = { - REQUEST_TOKEN_CALLBACK: requestCallback, - REFRESH_TOKEN_CALLBACK: refreshCallback - }; - // 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 () { - cache.clear(); - client = new MongoClient('mongodb://localhost/?authMechanism=MONGODB-OIDC', { - authMechanismProperties: authMechanismProperties - }); - await client.db('test').collection('test').findOne(); - cache.clear(); - await setupFailPoint(); - }); - - afterEach(async function () { - await removeFailPoint(); - 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: - // - // { - // "configureFailPoint": "failCommand", - // "mode": { - // "times": 2 - // }, - // "data": { - // "failCommands": [ - // "find", "saslStart" - // ], - // "errorCode": 391 - // } - // } - // - // Perform a find operation that fails. - // Close the client. - it('fails authentication', async function () { - try { - await client.db('test').collection('test').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); - } - }); - }); - }); - }); -}); diff --git a/test/mongodb.ts b/test/mongodb.ts index 53ea38256c0..718b0f5b905 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -105,14 +105,6 @@ export * from '../src/cmap/auth/gssapi'; 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/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/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..300268a43d1 100644 --- a/test/spec/auth/legacy/connection-string.json +++ b/test/spec/auth/legacy/connection-string.json @@ -480,133 +480,6 @@ "AWS_SESSION_TOKEN": "token!@#$%^&*()_+" } } - }, - { - "description": "should recognise the mechanism and request callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism when auth source is explicitly specified and with request callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism with request and refresh callback (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest", "oidcRefresh"], - "valid": true, - "credential": { - "username": null, - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true, - "REFRESH_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism and username with request callback (MONGODB-OIDC)", - "uri": "mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRequest"], - "valid": true, - "credential": { - "username": "principalName", - "password": null, - "source": "$external", - "mechanism": "MONGODB-OIDC", - "mechanism_properties": { - "REQUEST_TOKEN_CALLBACK": true - } - } - }, - { - "description": "should recognise the mechanism with aws device (MONGODB-OIDC)", - "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", - "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", - "valid": false, - "credential": null - }, - { - "description": "should throw an exception if neither deviceName nor callbacks specified (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "valid": false, - "credential": null - }, - { - "description": "should throw an exception when only refresh callback is specified (MONGODB-OIDC)", - "uri": "mongodb://localhost/?authMechanism=MONGODB-OIDC", - "callback": ["oidcRefresh"], - "valid": false, - "credential": null - }, - { - "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": null } ] } diff --git a/test/spec/auth/legacy/connection-string.yml b/test/spec/auth/legacy/connection-string.yml index 9f8aab4a725..33247820b39 100644 --- a/test/spec/auth/legacy/connection-string.yml +++ b/test/spec/auth/legacy/connection-string.yml @@ -350,107 +350,3 @@ tests: mechanism: MONGODB-AWS mechanism_properties: AWS_SESSION_TOKEN: token!@#$%^&*()_+ -- description: should recognise the mechanism and request callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism when auth source is explicitly specified - and with request callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authSource=$external - callback: - - oidcRequest - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism with request and refresh callback (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - - oidcRefresh - valid: true - credential: - username: - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true - REFRESH_TOKEN_CALLBACK: true -- description: should recognise the mechanism and username with request callback (MONGODB-OIDC) - uri: mongodb://principalName@localhost/?authMechanism=MONGODB-OIDC - callback: - - oidcRequest - valid: true - credential: - username: principalName - password: - source: "$external" - mechanism: MONGODB-OIDC - mechanism_properties: - REQUEST_TOKEN_CALLBACK: true -- description: should recognise the mechanism with aws device (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws - valid: true - credential: - username: - password: - 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 - 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 - valid: false - credential: -- description: should throw an exception if neither deviceName nor callbacks specified - (MONGODB-OIDC) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC - valid: false - credential: -- description: should throw an exception when only refresh callback is 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) - uri: mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=UnsupportedProperty:unexisted - valid: false - credential: 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 121244688e9..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: { PROVIDER_NAME: '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: { PROVIDER_NAME: 'iLoveJavaScript' } - }), - {} - ) - ) - .catch(error => error); - - expect(error).to.be.instanceOf(MongoInvalidArgumentError); - expect(error).to.match(/workflow for provider/); - }); - }); -}); 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_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/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/connection_string.test.ts b/test/unit/connection_string.test.ts index 822e8132721..07f65dd9e13 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -5,7 +5,6 @@ import * as sinon from 'sinon'; import { AUTH_MECHS_AUTH_SRC_EXTERNAL, AuthMechanism, - DEFAULT_ALLOWED_HOSTS, FEATURE_FLAGS, MongoAPIError, MongoClient, @@ -212,88 +211,6 @@ describe('Connection String', function () { expect(options.readConcern.level).to.equal('local'); }); - context('when auth mechanism is MONGODB-OIDC', function () { - context('when ALLOWED_HOSTS is in the URI', function () { - it('raises an error', function () { - expect(() => { - parseOptions( - 'mongodb://localhost/?authMechanismProperties=PROVIDER_NAME:aws,ALLOWED_HOSTS:[localhost]&authMechanism=MONGODB-OIDC' - ); - }).to.throw( - MongoParseError, - 'Auth mechanism property ALLOWED_HOSTS is not allowed in the connection string.' - ); - }); - }); - - context('when ALLOWED_HOSTS is in the options', function () { - context('when it is an array of strings', function () { - const hosts = ['*.example.com']; - - it('sets the allowed hosts property', function () { - const options = parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', - { - authMechanismProperties: { - ALLOWED_HOSTS: hosts - } - } - ); - expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'aws', - ALLOWED_HOSTS: hosts - }); - }); - }); - - context('when it is not an array of strings', function () { - it('raises an error', function () { - expect(() => { - parseOptions( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:aws', - { - authMechanismProperties: { - ALLOWED_HOSTS: [1, 2, 3] - } - } - ); - }).to.throw( - MongoInvalidArgumentError, - 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.' - ); - }); - }); - }); - - 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' - ); - expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'aws', - ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS - }); - }); - }); - - context('when TOKEN_AUDIENCE 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' - ); - - it('parses the uri', function () { - expect(options.credentials.mechanismProperties).to.deep.equal({ - PROVIDER_NAME: 'azure', - TOKEN_AUDIENCE: 'api://test', - ALLOWED_HOSTS: DEFAULT_ALLOWED_HOSTS - }); - }); - }); - }); - }); - it('should parse `authMechanismProperties`', function () { const options = parseOptions( 'mongodb://user%40EXAMPLE.COM:secret@localhost/?authMechanismProperties=SERVICE_NAME:other,SERVICE_REALM:blah,CANONICALIZE_HOST_NAME:true,SERVICE_HOST:example.com&authMechanism=GSSAPI' @@ -497,7 +414,7 @@ describe('Connection String', function () { it('should validate authMechanism', function () { expect(() => parseOptions('mongodb://localhost/?authMechanism=DOGS')).to.throw( MongoParseError, - 'authMechanism one of MONGODB-AWS,MONGODB-CR,DEFAULT,GSSAPI,PLAIN,SCRAM-SHA-1,SCRAM-SHA-256,MONGODB-X509,MONGODB-OIDC, got DOGS' + 'authMechanism one of MONGODB-AWS,MONGODB-CR,DEFAULT,GSSAPI,PLAIN,SCRAM-SHA-1,SCRAM-SHA-256,MONGODB-X509, got DOGS' ); }); @@ -549,14 +466,11 @@ describe('Connection String', function () { it(`should set authSource to $external for ${mechanism} external mechanism`, async function () { makeStub('authSource=thisShouldNotBeAuthSource'); const mechanismProperties = {}; - if (mechanism === AuthMechanism.MONGODB_OIDC) { - mechanismProperties.PROVIDER_NAME = 'aws'; - } const credentials = new MongoCredentials({ source: '$external', mechanism, - username: mechanism === AuthMechanism.MONGODB_OIDC ? undefined : 'username', + username: 'username', password: mechanism === AuthMechanism.MONGODB_X509 ? undefined : 'password', mechanismProperties: mechanismProperties }); diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index fc5cfec41fa..833082b9832 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -65,7 +65,6 @@ const EXPECTED_EXPORTS = [ 'MinKey', 'MongoAPIError', 'MongoAWSError', - 'MongoAzureError', 'MongoBatchReExecutionError', 'MongoBulkWriteError', 'MongoChangeStreamError', diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index e3d82c50bde..6fcfe796565 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -6,7 +6,6 @@ import { compareObjectId, eachAsync, HostAddress, - hostMatchesWildcards, isHello, LEGACY_HELLO_COMMAND, List, @@ -20,148 +19,6 @@ import { } from '../mongodb'; describe('driver utils', function () { - describe('.hostMatchesWildcards', function () { - context('when using domains', function () { - context('when using exact match', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost', ['localhost', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost', ['test1', 'test2'])).to.be.false; - }); - }); - - context('when the host matches a FQDN', function () { - it('returns true', function () { - expect(hostMatchesWildcards('mongodb.net', ['mongodb.net', 'other'])).to.be.true; - }); - }); - - context('when the host does not match a FQDN', function () { - it('returns false', function () { - expect(hostMatchesWildcards('mongodb.net', ['mongodb.com', 'other'])).to.be.false; - }); - }); - - context('when the host matches a FQDN with subdomain', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('prod.mongodb.net', ['prod.mongodb.net', 'other']) - ).to.be.true; - }); - }); - - context('when the host does not match a FQDN with subdomain', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('prod.mongodb.net', ['dev.mongodb.net', 'prod.mongodb.com']) - ).to.be.false; - }); - }); - }); - - context('when using a leading * with domains', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('localhost', ['*.localhost', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('localhost', ['*.test1', 'test2'])).to.be.false; - }); - }); - - context('when the wildcard does not start with *.', function () { - it('returns false', function () { - expect(hostMatchesWildcards('evilmongodb.com', ['*mongodb.com', 'test2'])).to.be.false; - }); - }); - - context('when the host matches a FQDN', function () { - it('returns true', function () { - expect(hostMatchesWildcards('mongodb.net', ['*.mongodb.net', 'other'])).to.be.true; - }); - }); - - context('when the host does not match a FQDN', function () { - it('returns false', function () { - expect(hostMatchesWildcards('mongodb.net', ['*.mongodb.com', 'other'])).to.be.false; - }); - }); - - context('when the host matches a FQDN with subdomain', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('prod.mongodb.net', ['*.prod.mongodb.net', 'other']) - ).to.be.true; - }); - }); - - context('when the host does not match a FQDN with subdomain', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('prod.mongodb.net', ['*.dev.mongodb.net', '*.prod.mongodb.com']) - ).to.be.false; - }); - }); - }); - }); - - context('when using IP addresses', function () { - context('when using IPv4', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('127.0.0.1', ['127.0.0.1', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('127.0.0.1', ['127.0.0.2', 'test2'])).to.be.false; - }); - }); - }); - - context('when using IPv6', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect(hostMatchesWildcards('::1', ['::1', 'other'])).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect(hostMatchesWildcards('::1', ['::2', 'test2'])).to.be.false; - }); - }); - }); - }); - - context('when using unix domain sockets', function () { - context('when the host matches at least one', function () { - it('returns true', function () { - expect( - hostMatchesWildcards('/tmp/mongodb-27017.sock', ['*/mongodb-27017.sock', 'other']) - ).to.be.true; - }); - }); - - context('when the host does not match any', function () { - it('returns false', function () { - expect( - hostMatchesWildcards('/tmp/mongodb-27017.sock', ['*/mongod-27017.sock', 'test2']) - ).to.be.false; - }); - }); - }); - }); - context('eachAsync()', function () { it('should callback with an error', function (done) { eachAsync(