From 7fd4c027652fc2e108c911bc07c93ad8582eb61c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 12 May 2023 22:27:15 +0200 Subject: [PATCH 01/40] feat(NODE-4929): Add OIDC Azure workflow --- src/cmap/auth/mongodb_aws.ts | 62 +---------------- src/cmap/auth/mongodb_oidc.ts | 4 +- .../mongodb_oidc/azure_service_workflow.ts | 59 ++++++++++++++++ src/utils.ts | 68 +++++++++++++++++++ 4 files changed, 131 insertions(+), 62 deletions(-) create mode 100644 src/cmap/auth/mongodb_oidc/azure_service_workflow.ts diff --git a/src/cmap/auth/mongodb_aws.ts b/src/cmap/auth/mongodb_aws.ts index 775979c5c6d..57e3a028ff8 100644 --- a/src/cmap/auth/mongodb_aws.ts +++ b/src/cmap/auth/mongodb_aws.ts @@ -1,6 +1,4 @@ import * as crypto from 'crypto'; -import * as http from 'http'; -import * as url from 'url'; import { promisify } from 'util'; import type { Binary, BSONSerializeOptions } from '../../bson'; @@ -12,7 +10,7 @@ import { MongoMissingCredentialsError, MongoRuntimeError } from '../../error'; -import { ByteUtils, maxWireVersion, ns } from '../../utils'; +import { ByteUtils, maxWireVersion, ns, request } from '../../utils'; import { type AuthContext, AuthProvider } from './auth_provider'; import { MongoCredentials } from './mongo_credentials'; import { AuthMechanism } from './providers'; @@ -253,61 +251,3 @@ function deriveRegion(host: string) { return parts[1]; } - -interface RequestOptions { - json?: boolean; - method?: string; - timeout?: number; - headers?: http.OutgoingHttpHeaders; -} - -async function request(uri: string): Promise>; -async function request( - uri: string, - options?: { json?: true } & RequestOptions -): Promise>; -async function request(uri: string, options?: { json: false } & RequestOptions): Promise; -async function request( - uri: string, - options: RequestOptions = {} -): Promise> { - return new Promise>((resolve, reject) => { - const requestOptions = { - method: 'GET', - timeout: 10000, - json: true, - ...url.parse(uri), - ...options - }; - - const req = http.request(requestOptions, res => { - res.setEncoding('utf8'); - - let data = ''; - res.on('data', d => { - data += d; - }); - - res.once('end', () => { - if (options.json === false) { - resolve(data); - return; - } - - try { - const parsed = JSON.parse(data); - resolve(parsed); - } catch { - // TODO(NODE-3483) - reject(new MongoRuntimeError(`Invalid JSON response: "${data}"`)); - } - }); - }); - - req.once('timeout', () => - req.destroy(new MongoAWSError(`AWS request to ${uri} timed out after ${options.timeout} ms`)) - ); - req.once('error', error => reject(error)); - req.end(); - }); -} diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index a85fc38c140..f3584c4893e 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -6,6 +6,7 @@ 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. */ @@ -60,7 +61,7 @@ export type OIDCRefreshFunction = ( context: OIDCCallbackContext ) => Promise; -type ProviderName = 'aws' | 'callback'; +type ProviderName = 'aws' | 'azure' | 'callback'; export interface Workflow { /** @@ -84,6 +85,7 @@ export interface Workflow { 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. diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts new file mode 100644 index 00000000000..fb5490856f2 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -0,0 +1,59 @@ +import { MongoAWSError } from '../../../error'; +import { request } from '../../../utils'; +import { ServiceWorkflow } from './service_workflow'; + +/** Error for when the token audience is missing in the environment. */ +const TOKEN_AUDIENCE_MISSING_ERROR = 'TOKEN_AUDIENCE must be set in the environment.'; + +/** 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' }); + +/** + * The Azure access token format. + */ +interface AzureAccessToken { + access_token: string; + expires_in: number; +} + +/** + * Device workflow implementation for Azure. + * + * @internal + */ +export class AzureServiceWorkflow extends ServiceWorkflow { + constructor() { + super(); + } + + /** + * Get the token from the environment. + */ + async getToken(): Promise { + const tokenAudience = process.env.TOKEN_AUDIENCE; + if (!tokenAudience) { + throw new MongoAWSError(TOKEN_AUDIENCE_MISSING_ERROR); + } + // TODO: Look for the token in the cache. They expire after 5 minutes. + const data = await getAzureTokenData(tokenAudience); + + // TODO: Validate access_token and expires_in are present. + return data.access_token; + } +} + +/** + * Hit the Azure endpoint to get the token data. + */ +async function getAzureTokenData(tokenAudience: string): Promise { + const url = `${AZURE_BASE_URL}&resource=${tokenAudience}`; + const data = await request(url, { + json: true, + headers: AZURE_HEADERS + }); + return data as AzureAccessToken; +} diff --git a/src/utils.ts b/src/utils.ts index 9c20cb4b53f..505f3bfd1d5 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,7 @@ import * as crypto from 'crypto'; import type { SrvRecord } from 'dns'; +import * as http from 'http'; +import * as url from 'url'; import { URL } from 'url'; import { type Document, ObjectId, resolveBSONOptions } from './bson'; @@ -14,6 +16,7 @@ import { type AnyError, MongoCompatibilityError, MongoInvalidArgumentError, + MongoNetworkTimeoutError, MongoNotConnectedError, MongoParseError, MongoRuntimeError @@ -1266,3 +1269,68 @@ export function matchesParentDomain(address: string, srvHost: string): boolean { return addressDomain.endsWith(srvHostDomain); } + +interface RequestOptions { + json?: boolean; + method?: string; + timeout?: number; + headers?: http.OutgoingHttpHeaders; +} + +export async function request(uri: string): Promise>; +export async function request( + uri: string, + options?: { json?: true } & RequestOptions +): Promise>; +export async function request( + uri: string, + options?: { json: false } & RequestOptions +): Promise; +export async function request( + uri: string, + options: RequestOptions = {} +): Promise> { + return new Promise>((resolve, reject) => { + const requestOptions = { + method: 'GET', + timeout: 10000, + json: true, + ...url.parse(uri), + ...options + }; + + const req = http.request(requestOptions, res => { + res.setEncoding('utf8'); + + let data = ''; + res.on('data', d => { + data += d; + }); + + res.once('end', () => { + if (options.json === false) { + resolve(data); + return; + } + + try { + const parsed = JSON.parse(data); + resolve(parsed); + } catch { + // TODO(NODE-3483) + reject(new MongoRuntimeError(`Invalid JSON response: "${data}"`)); + } + }); + }); + + req.once('timeout', () => + req.destroy( + new MongoNetworkTimeoutError( + `Network request to ${uri} timed out after ${options.timeout} ms` + ) + ) + ); + req.once('error', error => reject(error)); + req.end(); + }); +} From e15b4b8babe2ac06c2a24443a27915dab096612e Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 12 May 2023 23:43:39 +0200 Subject: [PATCH 02/40] refactor: token caches --- .../mongodb_oidc/azure_service_workflow.ts | 23 +++++++-- .../auth/mongodb_oidc/azure_token_cache.ts | 51 +++++++++++++++++++ src/cmap/auth/mongodb_oidc/cache.ts | 43 +++++++++++++++- .../auth/mongodb_oidc/callback_lock_cache.ts | 17 +++++-- .../auth/mongodb_oidc/callback_workflow.ts | 2 +- .../auth/mongodb_oidc/token_entry_cache.ts | 27 +++++----- 6 files changed, 137 insertions(+), 26 deletions(-) create mode 100644 src/cmap/auth/mongodb_oidc/azure_token_cache.ts diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index fb5490856f2..b92d9e9911d 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -1,5 +1,6 @@ import { MongoAWSError } from '../../../error'; import { request } from '../../../utils'; +import { AzureTokenCache } from './azure_token_cache'; import { ServiceWorkflow } from './service_workflow'; /** Error for when the token audience is missing in the environment. */ @@ -14,8 +15,9 @@ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/jso /** * The Azure access token format. + * @internal */ -interface AzureAccessToken { +export interface AzureAccessToken { access_token: string; expires_in: number; } @@ -26,8 +28,14 @@ interface AzureAccessToken { * @internal */ export class AzureServiceWorkflow extends ServiceWorkflow { + cache: AzureTokenCache; + + /** + * Instantiate the Azure service workflow. + */ constructor() { super(); + this.cache = new AzureTokenCache(); } /** @@ -39,10 +47,19 @@ export class AzureServiceWorkflow extends ServiceWorkflow { throw new MongoAWSError(TOKEN_AUDIENCE_MISSING_ERROR); } // TODO: Look for the token in the cache. They expire after 5 minutes. - const data = await getAzureTokenData(tokenAudience); + let token; + const entry = this.cache.getEntry(tokenAudience); + if (entry?.isValid()) { + token = entry.token; + } else { + this.cache.deleteEntry(tokenAudience); + const azureToken = await getAzureTokenData(tokenAudience); + const azureEntry = this.cache.addEntry(tokenAudience, azureToken); + token = azureEntry.token; + } // TODO: Validate access_token and expires_in are present. - return data.access_token; + return token; } } diff --git a/src/cmap/auth/mongodb_oidc/azure_token_cache.ts b/src/cmap/auth/mongodb_oidc/azure_token_cache.ts new file mode 100644 index 00000000000..1991e55a9e9 --- /dev/null +++ b/src/cmap/auth/mongodb_oidc/azure_token_cache.ts @@ -0,0 +1,51 @@ +import type { AzureAccessToken } from './azure_service_workflow'; +import { AbstractCache, 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 AbstractCache { + /** + * 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 from the address and username. + */ + 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 index 4a0a825bd4e..32b9fd5aa85 100644 --- a/src/cmap/auth/mongodb_oidc/cache.ts +++ b/src/cmap/auth/mongodb_oidc/cache.ts @@ -1,7 +1,41 @@ +/* 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 = 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; + } +} + +/** + * An OIDC token cache. + */ +export interface Cache { + /** + * Implement the cache key for the token. + */ + cacheKey(address: string, username: string, callbackHash: string): string; +} + /** * Base class for OIDC caches. */ -export abstract class Cache { +export abstract class AbstractCache implements Cache { entries: Map; /** @@ -18,10 +52,15 @@ export abstract class Cache { 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. */ - cacheKey(address: string, username: string, callbackHash: string): string { + 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 index 9e77b0614c5..ad1f3b4bac5 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -8,7 +8,7 @@ import type { OIDCRefreshFunction, OIDCRequestFunction } from '../mongodb_oidc'; -import { Cache } from './cache'; +import { AbstractCache } from './cache'; /** Error message for when request callback is missing. */ const REQUEST_CALLBACK_REQUIRED_ERROR = @@ -34,12 +34,12 @@ interface CallbacksEntry { /** * A cache of request and refresh callbacks per server/user. */ -export class CallbackLockCache extends Cache { +export class CallbackLockCache extends AbstractCache { /** * Get the callbacks for the connection and credentials. If an entry does not * exist a new one will get set. */ - getCallbacks(connection: Connection, credentials: MongoCredentials): CallbacksEntry { + getEntry(connection: Connection, credentials: MongoCredentials): CallbacksEntry { const requestCallback = credentials.mechanismProperties.REQUEST_TOKEN_CALLBACK; const refreshCallback = credentials.mechanismProperties.REFRESH_TOKEN_CALLBACK; if (!requestCallback) { @@ -51,13 +51,13 @@ export class CallbackLockCache extends Cache { if (entry) { return entry; } - return this.setCallbacks(key, callbackHash, requestCallback, refreshCallback); + return this.addEntry(key, callbackHash, requestCallback, refreshCallback); } /** * Set locked callbacks on for connection and credentials. */ - private setCallbacks( + private addEntry( key: string, callbackHash: string, requestCallback: OIDCRequestFunction, @@ -71,6 +71,13 @@ export class CallbackLockCache extends Cache { 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); + } } /** diff --git a/src/cmap/auth/mongodb_oidc/callback_workflow.ts b/src/cmap/auth/mongodb_oidc/callback_workflow.ts index 3ef1251fc43..c220ae5b70c 100644 --- a/src/cmap/auth/mongodb_oidc/callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/callback_workflow.ts @@ -65,7 +65,7 @@ export class CallbackWorkflow implements Workflow { response?: Document ): Promise { // Get the callbacks with locks from the callback lock cache. - const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getCallbacks( + const { requestCallback, refreshCallback, callbackHash } = this.callbackCache.getEntry( connection, credentials ); diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 0c24838a5dc..02ce03c9a83 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,31 +1,21 @@ import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc'; -import { Cache } from './cache'; +import { AbstractCache, ExpiringCacheEntry } from './cache'; -/* 5 minutes in milliseconds */ -const EXPIRATION_BUFFER_MS = 300000; /* Default expiration is now for when no expiration provided */ const DEFAULT_EXPIRATION_SECS = 0; + /** @internal */ -export class TokenEntry { +export class TokenEntry extends ExpiringCacheEntry { tokenResult: IdPServerResponse; serverInfo: IdPServerInfo; - expiration: number; /** * Instantiate the entry. */ constructor(tokenResult: IdPServerResponse, serverInfo: IdPServerInfo, expiration: number) { + super(expiration); this.tokenResult = tokenResult; this.serverInfo = serverInfo; - this.expiration = 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; } } @@ -33,7 +23,7 @@ export class TokenEntry { * Cache of OIDC token entries. * @internal */ -export class TokenEntryCache extends Cache { +export class TokenEntryCache extends AbstractCache { /** * Set an entry in the token cache. */ @@ -77,6 +67,13 @@ export class TokenEntryCache extends Cache { } } } + + /** + * Create a cache key from the address and username. + */ + cacheKey(address: string, username: string, callbackHash: string): string { + return this.hashedCacheKey(address, username, callbackHash); + } } /** From cb72e60d560bb93b7519b0364b9458e514a4038d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 16 May 2023 22:56:43 +0200 Subject: [PATCH 03/40] test: update configs --- .evergreen/config.in.yml | 45 +++++++++++ .evergreen/config.yml | 46 +++++++++++ .evergreen/generate_evergreen_tasks.js | 8 ++ .../mongodb_oidc/azure_service_workflow.ts | 26 ++++++- .../auth/mongodb_oidc/azure_token_cache.ts | 2 +- test/mongodb.ts | 2 + .../mongodb_oidc/azure_token_cache.test.ts | 77 +++++++++++++++++++ 7 files changed, 202 insertions(+), 4 deletions(-) create mode 100644 test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 1b65761e3f0..cf132836868 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1244,6 +1244,20 @@ tasks: args: - src/.evergreen/run-azure-kms-tests.sh + - name: "oidc-auth-test-azure-latest" + commands: + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + cd src + 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" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + task_groups: - name: serverless_task_group @@ -1348,6 +1362,37 @@ task_groups: tasks: - test-azurekms-task + - name: testazureoidc_task_group + setup_group: + - func: fetch source + # - func: prepare resources + # - func: fix absolute paths + # - func: make files executable + - 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 + pre: - func: "fetch source" - func: "windows fix" diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 540957dddb8..a6b63c5267f 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1168,6 +1168,19 @@ tasks: EXPECTED_AZUREKMS_OUTCOME: failure args: - src/.evergreen/run-azure-kms-tests.sh + - name: oidc-auth-test-azure-latest + commands: + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + cd src + 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" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh - name: test-latest-server tags: - latest @@ -3420,6 +3433,33 @@ 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 pre: - func: fetch source - func: windows fix @@ -3999,6 +4039,12 @@ 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-no-auth-tests display_name: No Auth Tests run_on: rhel80-large diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 78f660cc3cf..54d6222391c 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -754,6 +754,14 @@ 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: 'ubuntu2004-small', + batchtime: 20160, + tasks: ['testazureoidc_task_group'] +}); + BUILD_VARIANTS.push({ name: 'rhel8-no-auth-tests', display_name: 'No Auth Tests', diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index b92d9e9911d..b89dc088cc3 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -1,4 +1,4 @@ -import { MongoAWSError } from '../../../error'; +import { MongoAWSError, MongoMissingCredentialsError } from '../../../error'; import { request } from '../../../utils'; import { AzureTokenCache } from './azure_token_cache'; import { ServiceWorkflow } from './service_workflow'; @@ -13,6 +13,13 @@ const AZURE_BASE_URL = /** Azure request headers. */ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); +/** Properties allowed on results of the endpoint. */ +const RESULT_PROPERTIES = ['access_token', 'expires_in']; + +/** Invalid endpoint result error. */ +const ENDPOINT_RESULT_ERROR = + 'Azure endpoint did not return a value with only access_token and expires_in properties'; + /** * The Azure access token format. * @internal @@ -46,7 +53,6 @@ export class AzureServiceWorkflow extends ServiceWorkflow { if (!tokenAudience) { throw new MongoAWSError(TOKEN_AUDIENCE_MISSING_ERROR); } - // TODO: Look for the token in the cache. They expire after 5 minutes. let token; const entry = this.cache.getEntry(tokenAudience); if (entry?.isValid()) { @@ -58,7 +64,10 @@ export class AzureServiceWorkflow extends ServiceWorkflow { token = azureEntry.token; } - // TODO: Validate access_token and expires_in are present. + if (isEndpointResultInvalid(token)) { + this.cache.deleteEntry(tokenAudience); + throw new MongoMissingCredentialsError(ENDPOINT_RESULT_ERROR); + } return token; } } @@ -74,3 +83,14 @@ async function getAzureTokenData(tokenAudience: string): Promise RESULT_PROPERTIES.includes(prop)); +} diff --git a/src/cmap/auth/mongodb_oidc/azure_token_cache.ts b/src/cmap/auth/mongodb_oidc/azure_token_cache.ts index 1991e55a9e9..5115f7785f6 100644 --- a/src/cmap/auth/mongodb_oidc/azure_token_cache.ts +++ b/src/cmap/auth/mongodb_oidc/azure_token_cache.ts @@ -29,7 +29,7 @@ export class AzureTokenCache extends AbstractCache { } /** - * Create a cache key from the address and username. + * Create a cache key. */ cacheKey(tokenAudience: string): string { return tokenAudience; diff --git a/test/mongodb.ts b/test/mongodb.ts index 85ce656c815..53ea38256c0 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -107,6 +107,8 @@ 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'; 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 new file mode 100644 index 00000000000..6003a63b7ee --- /dev/null +++ b/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts @@ -0,0 +1,77 @@ +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.tokenResult).to.deep.equal(tokenResultWithExpiration); + }); + + 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')).to.equal(tokenResultWithExpiration); + }); + }); + + context('when there is no matching entry', function () { + it('returns undefined', function () { + expect(cache.getEntry('audience')).to.equal(undefined); + }); + }); + }); +}); From 31a5e1e0b43ead259794d0069b3779da69f1193c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 16 May 2023 23:10:11 +0200 Subject: [PATCH 04/40] fix: add azure error --- .../auth/mongodb_oidc/azure_service_workflow.ts | 6 +++--- src/error.ts | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index b89dc088cc3..e2f1bc4a477 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -1,4 +1,4 @@ -import { MongoAWSError, MongoMissingCredentialsError } from '../../../error'; +import { MongoAzureError } from '../../../error'; import { request } from '../../../utils'; import { AzureTokenCache } from './azure_token_cache'; import { ServiceWorkflow } from './service_workflow'; @@ -51,7 +51,7 @@ export class AzureServiceWorkflow extends ServiceWorkflow { async getToken(): Promise { const tokenAudience = process.env.TOKEN_AUDIENCE; if (!tokenAudience) { - throw new MongoAWSError(TOKEN_AUDIENCE_MISSING_ERROR); + throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); } let token; const entry = this.cache.getEntry(tokenAudience); @@ -66,7 +66,7 @@ export class AzureServiceWorkflow extends ServiceWorkflow { if (isEndpointResultInvalid(token)) { this.cache.deleteEntry(tokenAudience); - throw new MongoMissingCredentialsError(ENDPOINT_RESULT_ERROR); + throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } return token; } diff --git a/src/error.ts b/src/error.ts index 3357adfa773..f839cda2df2 100644 --- a/src/error.ts +++ b/src/error.ts @@ -390,6 +390,23 @@ 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. * From 71ba05e6d97f479b569c47526e8b256765368977 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 May 2023 16:04:10 +0200 Subject: [PATCH 05/40] test: point at new branch --- .evergreen/prepare-shell.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/prepare-shell.sh b/.evergreen/prepare-shell.sh index 4e2cfb0eafc..98bb0e99483 100644 --- a/.evergreen/prepare-shell.sh +++ b/.evergreen/prepare-shell.sh @@ -31,7 +31,7 @@ export PATH="$MONGODB_BINARIES:$PATH" if [ ! -d "$DRIVERS_TOOLS" ]; then # Only clone driver tools if it does not exist - git clone --depth=1 "https://github.com/mongodb-labs/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" + git clone --branch DRIVERS-2416 --depth=1 "https://github.com/blink1073/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" fi cat < "$MONGO_ORCHESTRATION_HOME/orchestration.config" From a8fa022609033c1e34d47a9769fdf1a76ef2fab1 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 May 2023 18:27:40 +0200 Subject: [PATCH 06/40] test: update permissions --- .evergreen/run-oidc-tests.sh | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 .evergreen/run-oidc-tests.sh diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh old mode 100644 new mode 100755 From b9a3ba43c51ce133e708394be6e8fb4552048f65 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 18 May 2023 19:10:45 +0200 Subject: [PATCH 07/40] test: fix unit test --- .../unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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 index f7e79081426..d10490fa5b0 100644 --- a/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/callback_lock_cache.test.ts @@ -25,7 +25,7 @@ describe('CallbackLockCache', function () { it('raises an error', function () { try { - cache.getCallbacks(connection, credentials); + cache.getEntry(connection, credentials); expect.fail('Must raise error when no request callback exists.'); } catch (error) { expect(error).to.be.instanceOf(MongoInvalidArgumentError); @@ -71,7 +71,7 @@ describe('CallbackLockCache', function () { } }); const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback, callbackHash } = cache.getCallbacks( + const { requestCallback, refreshCallback, callbackHash } = cache.getEntry( connection, credentials ); @@ -120,7 +120,7 @@ describe('CallbackLockCache', function () { } }); const cache = new CallbackLockCache(); - const { requestCallback, refreshCallback, callbackHash } = cache.getCallbacks( + const { requestCallback, refreshCallback, callbackHash } = cache.getEntry( connection, credentials ); From 8cfa7562da2792abca81379a454cd5f6b06eb092 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 19 May 2023 14:11:06 +0200 Subject: [PATCH 08/40] test: setup scripts --- .evergreen/run-oidc-tests.sh | 12 +++++++----- package.json | 1 + test/manual/mongodb_oidc_azure.prose.test.ts | 7 +++++++ 3 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 test/manual/mongodb_oidc_azure.prose.test.ts diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 2a4892ddf6a..9fd4f5262d9 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -2,14 +2,16 @@ 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"} -MONGODB_URI_SINGLE="${MONGODB_URI}/?authMechanism=MONGODB-OIDC&authMechanismProperties=DEVICE_NAME:aws" -echo $MONGODB_URI_SINGLE - -export MONGODB_URI="$MONGODB_URI_SINGLE" export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} -npm run check:oidc +if [ "$PROVIDER_NAME" = "azure" ]; then + npm run check:oidc-azure +else + npm run check:oidc +fi \ No newline at end of file diff --git a/package.json b/package.json index ea49b3fc052..efcbc2f8700 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", "check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts", + "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc_azure.prose.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js", diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts new file mode 100644 index 00000000000..383bc55856c --- /dev/null +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -0,0 +1,7 @@ +import { expect } from 'chai'; + +describe('MONGODB-OIDC (Azure)', function () { + it('passes', function () { + expect(true).to.be.true; + }); +}); From 42ddd33ce59ddd7b93be2b206b5c7aae92f64c59 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 19 May 2023 14:38:21 +0200 Subject: [PATCH 09/40] test: install deps --- .evergreen/config.in.yml | 24 ++++++++++++------------ .evergreen/config.yml | 1 + 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index cf132836868..22e1424881d 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1246,18 +1246,18 @@ tasks: - name: "oidc-auth-test-azure-latest" commands: - - command: shell.exec - params: - shell: bash - script: |- - set -o errexit - ${PREPARE_SHELL} - cd src - 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" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh - + - func: "install dependencies" + - command: shell.exec + params: + shell: bash + script: |- + set -o errexit + ${PREPARE_SHELL} + cd src + 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" + bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh task_groups: - name: serverless_task_group diff --git a/.evergreen/config.yml b/.evergreen/config.yml index a6b63c5267f..b331040e652 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1170,6 +1170,7 @@ tasks: - src/.evergreen/run-azure-kms-tests.sh - name: oidc-auth-test-azure-latest commands: + - func: install dependencies - command: shell.exec params: shell: bash From 5614f4515554fdbdfab8a145ef532ba9215b974a Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 22 May 2023 23:07:07 +0200 Subject: [PATCH 10/40] test: fix unit tests --- src/cmap/auth/mongodb_oidc/cache.ts | 9 ++++++++- src/cmap/auth/mongodb_oidc/token_entry_cache.ts | 9 +-------- .../cmap/auth/mongodb_oidc/azure_token_cache.test.ts | 4 ++-- .../cmap/auth/mongodb_oidc/token_entry_cache.test.ts | 4 ++-- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/cache.ts b/src/cmap/auth/mongodb_oidc/cache.ts index 32b9fd5aa85..69fb83144ba 100644 --- a/src/cmap/auth/mongodb_oidc/cache.ts +++ b/src/cmap/auth/mongodb_oidc/cache.ts @@ -11,7 +11,7 @@ export abstract class ExpiringCacheEntry { * Create a new expiring token entry. */ constructor(expiration: number) { - this.expiration = expiration; + this.expiration = this.expirationTime(expiration); } /** * The entry is still valid if the expiration is more than @@ -20,6 +20,13 @@ export abstract class ExpiringCacheEntry { isValid() { return this.expiration - Date.now() > EXPIRATION_BUFFER_MS; } + + /** + * Get an expiration time in milliseconds past epoch. Defaults to immediate. + */ + private expirationTime(expiresInSeconds: number): number { + return Date.now() + expiresInSeconds * 1000; + } } /** diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 02ce03c9a83..53d57343dee 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -37,7 +37,7 @@ export class TokenEntryCache extends AbstractCache { const entry = new TokenEntry( tokenResult, serverInfo, - expirationTime(tokenResult.expiresInSeconds) + tokenResult.expiresInSeconds ?? DEFAULT_EXPIRATION_SECS ); this.entries.set(this.cacheKey(address, username, callbackHash), entry); return entry; @@ -75,10 +75,3 @@ export class TokenEntryCache extends AbstractCache { return this.hashedCacheKey(address, username, callbackHash); } } - -/** - * Get an expiration time in milliseconds past epoch. Defaults to immediate. - */ -function expirationTime(expiresInSeconds?: number): number { - return Date.now() + (expiresInSeconds ?? DEFAULT_EXPIRATION_SECS) * 1000; -} 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 index 6003a63b7ee..ac95eb8a9c3 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_token_cache.test.ts @@ -19,7 +19,7 @@ describe('AzureTokenCache', function () { }); it('adds the token result', function () { - expect(entry.tokenResult).to.deep.equal(tokenResultWithExpiration); + expect(entry.token).to.equal('test'); }); it('creates an expiration', function () { @@ -64,7 +64,7 @@ describe('AzureTokenCache', function () { context('when there is a matching entry', function () { it('returns the entry', function () { - expect(cache.getEntry('audience1')).to.equal(tokenResultWithExpiration); + expect(cache.getEntry('audience1')?.token).to.equal('test'); }); }); 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 index bb79fa6530a..90f3a940858 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_entry_cache.test.ts @@ -48,7 +48,7 @@ describe('TokenEntryCache', function () { }); it('sets an immediate expiration', function () { - expect(entry.expiration).to.be.at.most(Date.now()); + expect(entry?.expiration).to.be.at.most(Date.now()); }); }); @@ -67,7 +67,7 @@ describe('TokenEntryCache', function () { }); it('sets an immediate expiration', function () { - expect(entry.expiration).to.be.at.most(Date.now()); + expect(entry?.expiration).to.be.at.most(Date.now()); }); }); }); From 3e7c17a44379ff1afb1f63394099bbe56e0b75df Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 23 May 2023 00:03:28 +0200 Subject: [PATCH 11/40] fix: make azure error public --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index 12346002ed3..881029d524e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,7 @@ export { ChangeStreamCursor } from './cursor/change_stream_cursor'; export { MongoAPIError, MongoAWSError, + MongoAzureError, MongoBatchReExecutionError, MongoChangeStreamError, MongoCompatibilityError, From 7ce4c4de90abd6ba4d5444d378639774fb9841e8 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 09:53:42 -0400 Subject: [PATCH 12/40] test: first 2 prose tests --- test/manual/mongodb_oidc_azure.prose.test.ts | 100 ++++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index 383bc55856c..c2e68626afe 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -1,7 +1,101 @@ import { expect } from 'chai'; -describe('MONGODB-OIDC (Azure)', function () { - it('passes', function () { - expect(true).to.be.true; +import { Collection, MongoClient } from '../mongodb'; + +describe('OIDC Auth Spec Prose Tests', function () { + describe('3. Azure Automatic Auth', function () { + let client: MongoClient; + let collection: Collection; + + afterEach(async function () { + await client?.close(); + }); + + describe('3.1 Connect', function () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' + ); + 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 () { + before(function () { + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:', + { + 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 () { + // 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. + }); + + describe('3.4 Azure Cache is Used', function () { + // 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. + }); + + describe('3.5 Reauthentication Succeeds', function () { + // 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. + }); }); }); From 5c80981aa56477ed785b136f1854285f68b4d451 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 10:11:49 -0400 Subject: [PATCH 13/40] test: fix validation, more prose tests --- src/cmap/auth/mongo_credentials.ts | 7 +++-- test/manual/mongodb_oidc_azure.prose.test.ts | 33 +++++++++++++++++++- test/tools/runner/hooks/configuration.js | 1 + 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 9239cc171b0..77f76812b82 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -30,6 +30,7 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism { return AuthMechanism.MONGODB_CR; } +const ALLOWED_PROVIDER_NAMES = ['aws', 'azure']; const ALLOWED_HOSTS_ERROR = 'Auth mechanism property ALLOWED_HOSTS must be an array of strings.'; /** @internal */ @@ -178,10 +179,12 @@ export class MongoCredentials { if ( this.mechanismProperties.PROVIDER_NAME && - this.mechanismProperties.PROVIDER_NAME !== 'aws' + !ALLOWED_PROVIDER_NAMES.includes(this.mechanismProperties.PROVIDER_NAME) ) { throw new MongoInvalidArgumentError( - `Currently only a PROVIDER_NAME of 'aws' is supported for mechanism '${this.mechanism}'.` + `Currently only a PROVIDER_NAME in ${ALLOWED_PROVIDER_NAMES.join( + ',' + )} is supported for mechanism '${this.mechanism}'.` ); } diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index c2e68626afe..777e62421bc 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -1,8 +1,11 @@ import { expect } from 'chai'; -import { Collection, MongoClient } from '../mongodb'; +import { Collection, 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; @@ -52,19 +55,47 @@ describe('OIDC Auth Spec Prose Tests', function () { }); describe('3.3 Main Cache Not Used', function () { + before(function () { + callbackCache?.clear(); + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' + ); + 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 () { + before(function () { + callbackCache?.clear(); + azureCache?.clear(); + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' + ); + 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 () { diff --git a/test/tools/runner/hooks/configuration.js b/test/tools/runner/hooks/configuration.js index 889870312b2..45351dbef11 100644 --- a/test/tools/runner/hooks/configuration.js +++ b/test/tools/runner/hooks/configuration.js @@ -155,6 +155,7 @@ const testConfigBeforeHook = async function () { serverApi: MONGODB_API_VERSION, atlas: process.env.ATLAS_CONNECTIVITY != null, aws: MONGODB_URI.includes('authMechanism=MONGODB-AWS'), + azure: MONGODB_URI.includes('PROVIDER_NAME:azure'), adl: this.configuration.buildInfo.dataLake ? this.configuration.buildInfo.dataLake.version : false, From 3e65c3fb2d5dd5be6457e1acbf7b05be13e67ba0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 10:20:54 -0400 Subject: [PATCH 14/40] test: add reauth test --- test/manual/mongodb_oidc_azure.prose.test.ts | 75 ++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index 777e62421bc..f53b2949277 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -99,6 +99,72 @@ describe('OIDC Auth Spec Prose Tests', function () { }); 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' + }); + }; + + before(async function () { + azureCache?.clear(); + client = new MongoClient( + 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' + ); + await client.db('test').collection('test').findOne(); + addListeners(); + setupFailPoint(); + }); + + afterEach(async function () { + await removeFailPoint(); + await client.close(); + }); + // 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. @@ -127,6 +193,15 @@ describe('OIDC Auth Spec Prose Tests', function () { //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']); + }); }); }); }); From 0af4f95dc716459d4795228f0c8a545f581933e0 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 10:30:19 -0400 Subject: [PATCH 15/40] test: debug tests --- test/manual/mongodb_oidc_azure.prose.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index f53b2949277..2771fb7119e 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -3,8 +3,14 @@ import { expect } from 'chai'; import { Collection, 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; + let callbackCache; + let azureCache; + + before(function () { + console.log('OIDC_WORKFLOWS', OIDC_WORKFLOWS); + callbackCache = OIDC_WORKFLOWS.get('callback').cache; + azureCache = OIDC_WORKFLOWS.get('azure)').cache; + }); describe('3. Azure Automatic Auth', function () { let client: MongoClient; From 31a0b374e18fae4a8f89b2f36c7dc93f85e821fd Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 11:20:10 -0400 Subject: [PATCH 16/40] test: update cache getter --- test/manual/mongodb_oidc_azure.prose.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index 2771fb7119e..35ec3906338 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -3,14 +3,8 @@ import { expect } from 'chai'; import { Collection, MongoClient, OIDC_WORKFLOWS } from '../mongodb'; describe('OIDC Auth Spec Prose Tests', function () { - let callbackCache; - let azureCache; - - before(function () { - console.log('OIDC_WORKFLOWS', OIDC_WORKFLOWS); - callbackCache = OIDC_WORKFLOWS.get('callback').cache; - azureCache = OIDC_WORKFLOWS.get('azure)').cache; - }); + const callbackCache = OIDC_WORKFLOWS.get('callback').cache; + const azureCache = OIDC_WORKFLOWS.get('azure').cache; describe('3. Azure Automatic Auth', function () { let client: MongoClient; From 2b0bf488c42a7afc5f6423ab09c754d1c510a172 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 11:45:08 -0400 Subject: [PATCH 17/40] test: get token audience from props --- src/cmap/auth/mongo_credentials.ts | 16 +++++++++++++++- .../auth/mongodb_oidc/azure_service_workflow.ts | 12 +++++++----- src/cmap/auth/mongodb_oidc/service_workflow.ts | 6 +++--- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 77f76812b82..590cc557981 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -2,6 +2,7 @@ import type { Document } from '../../bson'; import { MongoAPIError, + MongoAzureError, MongoInvalidArgumentError, MongoMissingCredentialsError } from '../../error'; @@ -43,6 +44,10 @@ export const DEFAULT_ALLOWED_HOSTS = [ '::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; @@ -55,9 +60,11 @@ export interface AuthMechanismProperties extends Document { /** @experimental */ REFRESH_TOKEN_CALLBACK?: OIDCRefreshFunction; /** @experimental */ - PROVIDER_NAME?: 'aws'; + PROVIDER_NAME?: 'aws' | 'azure'; /** @experimental */ ALLOWED_HOSTS?: string[]; + /** @experimental */ + TOKEN_AUDIENCE?: string; } /** @public */ @@ -177,6 +184,13 @@ export class MongoCredentials { ); } + 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) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index e2f1bc4a477..0ff332474a3 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -1,11 +1,9 @@ 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'; -/** Error for when the token audience is missing in the environment. */ -const TOKEN_AUDIENCE_MISSING_ERROR = 'TOKEN_AUDIENCE must be set in the environment.'; - /** Base URL for getting Azure tokens. */ const AZURE_BASE_URL = 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01'; @@ -20,6 +18,10 @@ const RESULT_PROPERTIES = ['access_token', 'expires_in']; 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 @@ -48,8 +50,8 @@ export class AzureServiceWorkflow extends ServiceWorkflow { /** * Get the token from the environment. */ - async getToken(): Promise { - const tokenAudience = process.env.TOKEN_AUDIENCE; + async getToken(credentials?: MongoCredentials): Promise { + const tokenAudience = credentials?.mechanismProperties.TOKEN_AUDIENCE; if (!tokenAudience) { throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); } diff --git a/src/cmap/auth/mongodb_oidc/service_workflow.ts b/src/cmap/auth/mongodb_oidc/service_workflow.ts index 4c3e5bb3164..fb01e2c24ce 100644 --- a/src/cmap/auth/mongodb_oidc/service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/service_workflow.ts @@ -16,7 +16,7 @@ export abstract class ServiceWorkflow implements Workflow { * and then attempts to read the token from that path. */ async execute(connection: Connection, credentials: MongoCredentials): Promise { - const token = await this.getToken(); + const token = await this.getToken(credentials); const command = commandDocument(token); return connection.commandAsync(ns(credentials.source), command, undefined); } @@ -25,7 +25,7 @@ export abstract class ServiceWorkflow implements Workflow { * Get the document to add for speculative authentication. */ async speculativeAuth(credentials: MongoCredentials): Promise { - const token = await this.getToken(); + const token = await this.getToken(credentials); const document = commandDocument(token); document.db = credentials.source; return { speculativeAuthenticate: document }; @@ -34,7 +34,7 @@ export abstract class ServiceWorkflow implements Workflow { /** * Get the token from the environment or endpoint. */ - abstract getToken(): Promise; + abstract getToken(credentials: MongoCredentials): Promise; } /** From a15ad09c522e1c29696f5918fdaa6f8cd5237f63 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 11:55:18 -0400 Subject: [PATCH 18/40] chore: debug --- src/cmap/auth/mongodb_oidc/azure_service_workflow.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index 0ff332474a3..ca3ffd36652 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -62,10 +62,11 @@ export class AzureServiceWorkflow extends ServiceWorkflow { } else { this.cache.deleteEntry(tokenAudience); const azureToken = await getAzureTokenData(tokenAudience); + console.log('azureToken', azureToken); const azureEntry = this.cache.addEntry(tokenAudience, azureToken); token = azureEntry.token; } - + console.log('token', token); if (isEndpointResultInvalid(token)) { this.cache.deleteEntry(tokenAudience); throw new MongoAzureError(ENDPOINT_RESULT_ERROR); From 38b5d7bf5d184555beff07e61a13b680cb621c22 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 12:26:36 -0400 Subject: [PATCH 19/40] test: update uris --- .evergreen/run-oidc-tests.sh | 24 ++++++++++--- .../mongodb_oidc/azure_service_workflow.ts | 1 + test/manual/mongodb_oidc_azure.prose.test.ts | 36 +++++++++---------- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 9fd4f5262d9..2849600e63b 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -10,8 +10,24 @@ MONGODB_URI=${MONGODB_URI:-"mongodb://127.0.0.1:27017"} export OIDC_TOKEN_DIR=${OIDC_TOKEN_DIR} -if [ "$PROVIDER_NAME" = "azure" ]; then - npm run check:oidc-azure -else +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 -fi \ No newline at end of file +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 +fi diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index ca3ffd36652..dec6dcf4a18 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -80,6 +80,7 @@ export class AzureServiceWorkflow extends ServiceWorkflow { */ async function getAzureTokenData(tokenAudience: string): Promise { const url = `${AZURE_BASE_URL}&resource=${tokenAudience}`; + console.log('url', url); const data = await request(url, { json: true, headers: AZURE_HEADERS diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index 35ec3906338..f8cd2bf00b8 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -1,6 +1,13 @@ import { expect } from 'chai'; -import { Collection, MongoClient, OIDC_WORKFLOWS } from '../mongodb'; +import { + Collection, + CommandFailedEvent, + CommandStartedEvent, + CommandSucceededEvent, + MongoClient, + OIDC_WORKFLOWS +} from '../mongodb'; describe('OIDC Auth Spec Prose Tests', function () { const callbackCache = OIDC_WORKFLOWS.get('callback').cache; @@ -16,9 +23,7 @@ describe('OIDC Auth Spec Prose Tests', function () { describe('3.1 Connect', function () { before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' - ); + client = new MongoClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -33,14 +38,11 @@ describe('OIDC Auth Spec Prose Tests', function () { describe('3.2 Allowed Hosts Ignored', function () { before(function () { - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:', - { - authMechanismProperties: { - ALLOWED_HOSTS: [] - } + client = new MongoClient(process.env.MONGODB_URI, { + authMechanismProperties: { + ALLOWED_HOSTS: [] } - ); + }); collection = client.db('test').collection('test'); }); @@ -57,9 +59,7 @@ describe('OIDC Auth Spec Prose Tests', function () { describe('3.3 Main Cache Not Used', function () { before(function () { callbackCache?.clear(); - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' - ); + client = new MongoClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -79,9 +79,7 @@ describe('OIDC Auth Spec Prose Tests', function () { before(function () { callbackCache?.clear(); azureCache?.clear(); - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' - ); + client = new MongoClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -152,9 +150,7 @@ describe('OIDC Auth Spec Prose Tests', function () { before(async function () { azureCache?.clear(); - client = new MongoClient( - 'mongodb://localhost/?authMechanism=MONGODB-OIDC&authMechanismProperties=PROVIDER_NAME:azure,TOKEN_AUDIENCE:' - ); + client = new MongoClient(process.env.MONGODB_URI); await client.db('test').collection('test').findOne(); addListeners(); setupFailPoint(); From 0409aa8551db399f4399bf4c24ea8f5fc5d9985d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 12:43:36 -0400 Subject: [PATCH 20/40] test: updatescript --- .evergreen/run-oidc-tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 2849600e63b..004e5d86138 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -28,6 +28,6 @@ elif [ "$PROVIDER_NAME" = "azure" ]; then 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}" + export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api://${AZUREOIDC_CLIENTID}" npm run check:oidc-azure fi From 2e28cca2080a4cd6682f5d50ea138f821fe0ad67 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 13:42:30 -0400 Subject: [PATCH 21/40] fix: token audience property --- .evergreen/run-oidc-tests.sh | 2 +- src/connection_string.ts | 2 +- test/unit/connection_string.test.ts | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 004e5d86138..2849600e63b 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -28,6 +28,6 @@ elif [ "$PROVIDER_NAME" = "azure" ]; then fi MONGODB_URI="${MONGODB_URI}/?authMechanism=MONGODB-OIDC" MONGODB_URI="${MONGODB_URI}&authMechanismProperties=PROVIDER_NAME:azure" - export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api://${AZUREOIDC_CLIENTID}" + export MONGODB_URI="${MONGODB_URI},TOKEN_AUDIENCE:api%3A%2F%2F${AZUREOIDC_CLIENTID}" npm run check:oidc-azure fi diff --git a/src/connection_string.ts b/src/connection_string.ts index d0f7c2ad7e1..789d8342e7e 100644 --- a/src/connection_string.ts +++ b/src/connection_string.ts @@ -207,7 +207,7 @@ function getUIntFromOptions(name: string, value: unknown): number { function* entriesFromString(value: string): Generator<[string, string]> { const keyValuePairs = value.split(','); for (const keyValue of keyValuePairs) { - const [key, value] = keyValue.split(':'); + const [key, value] = keyValue.split(/:(.*)/); if (value == null) { throw new MongoParseError('Cannot have undefined values in key value pairs'); } diff --git a/test/unit/connection_string.test.ts b/test/unit/connection_string.test.ts index 38e454e7086..822e8132721 100644 --- a/test/unit/connection_string.test.ts +++ b/test/unit/connection_string.test.ts @@ -276,6 +276,22 @@ describe('Connection String', function () { }); }); }); + + 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 () { From 339ec6853a30baea323b342aec4433183499b915 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 14:05:53 -0400 Subject: [PATCH 22/40] test: parse tokens --- .../auth/mongodb_oidc/azure_service_workflow.ts | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index dec6dcf4a18..cd3ab9b5585 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -11,9 +11,6 @@ const AZURE_BASE_URL = /** Azure request headers. */ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); -/** Properties allowed on results of the endpoint. */ -const RESULT_PROPERTIES = ['access_token', 'expires_in']; - /** Invalid endpoint result error. */ const ENDPOINT_RESULT_ERROR = 'Azure endpoint did not return a value with only access_token and expires_in properties'; @@ -61,10 +58,10 @@ export class AzureServiceWorkflow extends ServiceWorkflow { token = entry.token; } else { this.cache.deleteEntry(tokenAudience); - const azureToken = await getAzureTokenData(tokenAudience); - console.log('azureToken', azureToken); - const azureEntry = this.cache.addEntry(tokenAudience, azureToken); - token = azureEntry.token; + const response = await getAzureTokenData(tokenAudience); + console.log('response', response); + this.cache.addEntry(tokenAudience, response); + token = response.access_token; } console.log('token', token); if (isEndpointResultInvalid(token)) { @@ -95,6 +92,5 @@ async function getAzureTokenData(tokenAudience: string): Promise RESULT_PROPERTIES.includes(prop)); + return !('access_token' in token) || !('expires_in' in token); } From 722a81449c36797cf131bdc175699b45b4176146 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 14:21:08 -0400 Subject: [PATCH 23/40] test: fix logic --- src/cmap/auth/mongodb_oidc/azure_service_workflow.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index cd3ab9b5585..c80b83b90f4 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -53,18 +53,19 @@ export class AzureServiceWorkflow extends ServiceWorkflow { throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); } let token; + let response; const entry = this.cache.getEntry(tokenAudience); if (entry?.isValid()) { token = entry.token; } else { this.cache.deleteEntry(tokenAudience); - const response = await getAzureTokenData(tokenAudience); + response = await getAzureTokenData(tokenAudience); console.log('response', response); this.cache.addEntry(tokenAudience, response); token = response.access_token; } console.log('token', token); - if (isEndpointResultInvalid(token)) { + if (isEndpointResultInvalid(response)) { this.cache.deleteEntry(tokenAudience); throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } @@ -88,9 +89,9 @@ async function getAzureTokenData(tokenAudience: string): Promise Date: Thu, 25 May 2023 14:32:35 -0400 Subject: [PATCH 24/40] test: more debug --- src/cmap/auth/mongodb_oidc/azure_service_workflow.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index c80b83b90f4..2a2eb9450a0 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -92,6 +92,8 @@ async function getAzureTokenData(tokenAudience: string): Promise Date: Thu, 25 May 2023 14:46:09 -0400 Subject: [PATCH 25/40] test: move validation --- src/cmap/auth/mongodb_oidc/azure_service_workflow.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index 2a2eb9450a0..ffbe2e7eb16 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -53,22 +53,20 @@ export class AzureServiceWorkflow extends ServiceWorkflow { throw new MongoAzureError(TOKEN_AUDIENCE_MISSING_ERROR); } let token; - let response; const entry = this.cache.getEntry(tokenAudience); if (entry?.isValid()) { token = entry.token; } else { this.cache.deleteEntry(tokenAudience); - response = await getAzureTokenData(tokenAudience); + const response = await getAzureTokenData(tokenAudience); + if (isEndpointResultInvalid(response)) { + throw new MongoAzureError(ENDPOINT_RESULT_ERROR); + } console.log('response', response); this.cache.addEntry(tokenAudience, response); token = response.access_token; } console.log('token', token); - if (isEndpointResultInvalid(response)) { - this.cache.deleteEntry(tokenAudience); - throw new MongoAzureError(ENDPOINT_RESULT_ERROR); - } return token; } } @@ -92,8 +90,6 @@ async function getAzureTokenData(tokenAudience: string): Promise Date: Thu, 25 May 2023 16:02:22 -0400 Subject: [PATCH 26/40] test: await fail point --- test/manual/mongodb_oidc_azure.prose.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index f8cd2bf00b8..eb28591fcf8 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -102,6 +102,7 @@ describe('OIDC Auth Spec Prose Tests', function () { const commandFailedEvents: CommandFailedEvent[] = []; const commandStartedListener = event => { + console.log(event); if (event.commandName === 'find') { commandStartedEvents.push(event); } @@ -153,10 +154,10 @@ describe('OIDC Auth Spec Prose Tests', function () { client = new MongoClient(process.env.MONGODB_URI); await client.db('test').collection('test').findOne(); addListeners(); - setupFailPoint(); + await setupFailPoint(); }); - afterEach(async function () { + after(async function () { await removeFailPoint(); await client.close(); }); From fb316d7f32188ab1f18086da7420197d41d4d9e9 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 16:17:16 -0400 Subject: [PATCH 27/40] test: monitor commands --- test/manual/mongodb_oidc_azure.prose.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index eb28591fcf8..a0a3c65fc3d 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -151,15 +151,14 @@ describe('OIDC Auth Spec Prose Tests', function () { before(async function () { azureCache?.clear(); - client = new MongoClient(process.env.MONGODB_URI); + client = new MongoClient(process.env.MONGODB_URI, { monitorCommands: true }); await client.db('test').collection('test').findOne(); addListeners(); await setupFailPoint(); }); - after(async function () { + afterEach(async function () { await removeFailPoint(); - await client.close(); }); // Clear the Azure OIDC cache. From 25ec3da73ceb810c169d97e0a6b719c7225e8d76 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 16:27:03 -0400 Subject: [PATCH 28/40] chore: remove console logging --- src/cmap/auth/mongodb_oidc/azure_service_workflow.ts | 3 --- test/manual/mongodb_oidc_azure.prose.test.ts | 1 - 2 files changed, 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index ffbe2e7eb16..7df253c91bc 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -62,11 +62,9 @@ export class AzureServiceWorkflow extends ServiceWorkflow { if (isEndpointResultInvalid(response)) { throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } - console.log('response', response); this.cache.addEntry(tokenAudience, response); token = response.access_token; } - console.log('token', token); return token; } } @@ -76,7 +74,6 @@ export class AzureServiceWorkflow extends ServiceWorkflow { */ async function getAzureTokenData(tokenAudience: string): Promise { const url = `${AZURE_BASE_URL}&resource=${tokenAudience}`; - console.log('url', url); const data = await request(url, { json: true, headers: AZURE_HEADERS diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/manual/mongodb_oidc_azure.prose.test.ts index a0a3c65fc3d..90e5f479fb2 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/manual/mongodb_oidc_azure.prose.test.ts @@ -102,7 +102,6 @@ describe('OIDC Auth Spec Prose Tests', function () { const commandFailedEvents: CommandFailedEvent[] = []; const commandStartedListener = event => { - console.log(event); if (event.commandName === 'find') { commandStartedEvents.push(event); } From a32bd39e86e49f43ccd27f8b05d1c3561dc9eb92 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 16:39:39 -0400 Subject: [PATCH 29/40] test: fix script --- .evergreen/config.in.yml | 3 --- .evergreen/run-oidc-tests.sh | 2 ++ 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 22e1424881d..4027e96a603 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1365,9 +1365,6 @@ task_groups: - name: testazureoidc_task_group setup_group: - func: fetch source - # - func: prepare resources - # - func: fix absolute paths - # - func: make files executable - command: shell.exec params: shell: bash diff --git a/.evergreen/run-oidc-tests.sh b/.evergreen/run-oidc-tests.sh index 2849600e63b..98881a0c2d2 100755 --- a/.evergreen/run-oidc-tests.sh +++ b/.evergreen/run-oidc-tests.sh @@ -30,4 +30,6 @@ elif [ "$PROVIDER_NAME" = "azure" ]; then 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 From fce0ccddb8b9446aa184f3d05197d9b463131656 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Thu, 25 May 2023 16:51:34 -0400 Subject: [PATCH 30/40] test: fix unit test --- test/unit/index.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/index.test.ts b/test/unit/index.test.ts index 833082b9832..fc5cfec41fa 100644 --- a/test/unit/index.test.ts +++ b/test/unit/index.test.ts @@ -65,6 +65,7 @@ const EXPECTED_EXPORTS = [ 'MinKey', 'MongoAPIError', 'MongoAWSError', + 'MongoAzureError', 'MongoBatchReExecutionError', 'MongoBulkWriteError', 'MongoChangeStreamError', From d160ac512501ce426ff34c95283ba050bc0a7403 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 26 May 2023 08:51:40 -0400 Subject: [PATCH 31/40] fix: more mech properties type safety --- src/cmap/auth/mongo_credentials.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 590cc557981..150a0841680 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -1,4 +1,5 @@ // Resolves the default auth mechanism according to +// Resolves the default auth mechanism according to import type { Document } from '../../bson'; import { MongoAPIError, @@ -31,7 +32,7 @@ function getDefaultAuthMechanism(hello?: Document): AuthMechanism { return AuthMechanism.MONGODB_CR; } -const ALLOWED_PROVIDER_NAMES = ['aws', 'azure']; +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 */ From 95e17e7da1533bbde2594ac01bf0584905496e62 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 26 May 2023 10:51:27 -0400 Subject: [PATCH 32/40] test: use drivers tools master --- .evergreen/prepare-shell.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.evergreen/prepare-shell.sh b/.evergreen/prepare-shell.sh index 98bb0e99483..4e2cfb0eafc 100644 --- a/.evergreen/prepare-shell.sh +++ b/.evergreen/prepare-shell.sh @@ -31,7 +31,7 @@ export PATH="$MONGODB_BINARIES:$PATH" if [ ! -d "$DRIVERS_TOOLS" ]; then # Only clone driver tools if it does not exist - git clone --branch DRIVERS-2416 --depth=1 "https://github.com/blink1073/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" + git clone --depth=1 "https://github.com/mongodb-labs/drivers-evergreen-tools.git" "${DRIVERS_TOOLS}" fi cat < "$MONGO_ORCHESTRATION_HOME/orchestration.config" From 0b4e3b64bc39a2d1ae8108de2f39d05ce4c23b35 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 13:01:32 -0400 Subject: [PATCH 33/40] test: update per suggestions --- .evergreen/ci_matrix_constants.js | 2 ++ .evergreen/config.yml | 2 +- .evergreen/generate_evergreen_tasks.js | 3 +- .../mongodb_oidc/azure_service_workflow.ts | 26 +++++++---------- .../auth/mongodb_oidc/azure_token_cache.ts | 4 +-- src/cmap/auth/mongodb_oidc/cache.ts | 12 +------- .../auth/mongodb_oidc/callback_lock_cache.ts | 4 +-- .../auth/mongodb_oidc/token_entry_cache.ts | 4 +-- .../auth}/mongodb_oidc_azure.prose.test.ts | 29 ++++++++++++------- test/tools/runner/config.ts | 4 +++ 10 files changed, 44 insertions(+), 46 deletions(-) rename test/{manual => integration/auth}/mongodb_oidc_azure.prose.test.ts (89%) diff --git a/.evergreen/ci_matrix_constants.js b/.evergreen/ci_matrix_constants.js index 081a4082be6..ec3ca578659 100644 --- a/.evergreen/ci_matrix_constants.js +++ b/.evergreen/ci_matrix_constants.js @@ -17,6 +17,7 @@ const DEFAULT_OS = 'rhel80-large'; const WINDOWS_OS = 'windows-vsCurrent-large'; const MACOS_OS = 'macos-1100'; const UBUNTU_OS = 'ubuntu1804-large'; +const UBUNTU_20_OS = 'ubuntu1004-small' const DEBIAN_OS = 'debian11-small'; module.exports = { @@ -32,5 +33,6 @@ module.exports = { WINDOWS_OS, MACOS_OS, UBUNTU_OS, + UBUNTU_20_OS, DEBIAN_OS }; diff --git a/.evergreen/config.yml b/.evergreen/config.yml index b331040e652..8b54523fee3 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -4042,7 +4042,7 @@ buildvariants: - test-azurekms-fail-task - name: ubuntu20-test-azure-oidc display_name: Azure OIDC - run_on: ubuntu2004-small + run_on: ubuntu1004-small batchtime: 20160 tasks: - testazureoidc_task_group diff --git a/.evergreen/generate_evergreen_tasks.js b/.evergreen/generate_evergreen_tasks.js index 54d6222391c..74b3f98148f 100644 --- a/.evergreen/generate_evergreen_tasks.js +++ b/.evergreen/generate_evergreen_tasks.js @@ -16,6 +16,7 @@ const { WINDOWS_OS, MACOS_OS, UBUNTU_OS, + UBUNTU_20_OS, DEBIAN_OS } = require('./ci_matrix_constants'); @@ -757,7 +758,7 @@ BUILD_VARIANTS.push({ BUILD_VARIANTS.push({ name: 'ubuntu20-test-azure-oidc', display_name: 'Azure OIDC', - run_on: 'ubuntu2004-small', + run_on: UBUNTU_20_OS, batchtime: 20160, tasks: ['testazureoidc_task_group'] }); diff --git a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts index 7df253c91bc..fadbf5e9fd9 100644 --- a/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_service_workflow.ts @@ -34,15 +34,7 @@ export interface AzureAccessToken { * @internal */ export class AzureServiceWorkflow extends ServiceWorkflow { - cache: AzureTokenCache; - - /** - * Instantiate the Azure service workflow. - */ - constructor() { - super(); - this.cache = new AzureTokenCache(); - } + cache = new AzureTokenCache(); /** * Get the token from the environment. @@ -59,7 +51,7 @@ export class AzureServiceWorkflow extends ServiceWorkflow { } else { this.cache.deleteEntry(tokenAudience); const response = await getAzureTokenData(tokenAudience); - if (isEndpointResultInvalid(response)) { + if (!isEndpointResultValid(response)) { throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } this.cache.addEntry(tokenAudience, response); @@ -82,11 +74,13 @@ async function getAzureTokenData(tokenAudience: string): Promise { +export class AzureTokenCache extends Cache { /** * Add an entry to the cache. */ diff --git a/src/cmap/auth/mongodb_oidc/cache.ts b/src/cmap/auth/mongodb_oidc/cache.ts index 69fb83144ba..3e74adf1eff 100644 --- a/src/cmap/auth/mongodb_oidc/cache.ts +++ b/src/cmap/auth/mongodb_oidc/cache.ts @@ -29,20 +29,10 @@ export abstract class ExpiringCacheEntry { } } -/** - * An OIDC token cache. - */ -export interface Cache { - /** - * Implement the cache key for the token. - */ - cacheKey(address: string, username: string, callbackHash: string): string; -} - /** * Base class for OIDC caches. */ -export abstract class AbstractCache implements Cache { +export abstract class Cache { entries: Map; /** diff --git a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts index ad1f3b4bac5..b92a504b0a8 100644 --- a/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts +++ b/src/cmap/auth/mongodb_oidc/callback_lock_cache.ts @@ -8,7 +8,7 @@ import type { OIDCRefreshFunction, OIDCRequestFunction } from '../mongodb_oidc'; -import { AbstractCache } from './cache'; +import { Cache } from './cache'; /** Error message for when request callback is missing. */ const REQUEST_CALLBACK_REQUIRED_ERROR = @@ -34,7 +34,7 @@ interface CallbacksEntry { /** * A cache of request and refresh callbacks per server/user. */ -export class CallbackLockCache extends AbstractCache { +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. diff --git a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts index 53d57343dee..1b5b9de3314 100644 --- a/src/cmap/auth/mongodb_oidc/token_entry_cache.ts +++ b/src/cmap/auth/mongodb_oidc/token_entry_cache.ts @@ -1,5 +1,5 @@ import type { IdPServerInfo, IdPServerResponse } from '../mongodb_oidc'; -import { AbstractCache, ExpiringCacheEntry } from './cache'; +import { Cache, ExpiringCacheEntry } from './cache'; /* Default expiration is now for when no expiration provided */ const DEFAULT_EXPIRATION_SECS = 0; @@ -23,7 +23,7 @@ export class TokenEntry extends ExpiringCacheEntry { * Cache of OIDC token entries. * @internal */ -export class TokenEntryCache extends AbstractCache { +export class TokenEntryCache extends Cache { /** * Set an entry in the token cache. */ diff --git a/test/manual/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts similarity index 89% rename from test/manual/mongodb_oidc_azure.prose.test.ts rename to test/integration/auth/mongodb_oidc_azure.prose.test.ts index 90e5f479fb2..910a44d888d 100644 --- a/test/manual/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -7,7 +7,7 @@ import { CommandSucceededEvent, MongoClient, OIDC_WORKFLOWS -} from '../mongodb'; +} from '../../mongodb'; describe('OIDC Auth Spec Prose Tests', function () { const callbackCache = OIDC_WORKFLOWS.get('callback').cache; @@ -17,13 +17,20 @@ describe('OIDC Auth Spec Prose Tests', function () { let client: MongoClient; let collection: Collection; + beforeEach(function () { + if (!this.configuration.isAzureOIDC(process.env.MONGODB_URI)) { + this.currentTest?.skipReason = 'Azure OIDC prose tests require an Azure OIDC environment.'; + this.skip(); + } + }); + afterEach(async function () { await client?.close(); }); describe('3.1 Connect', function () { - before(function () { - client = new MongoClient(process.env.MONGODB_URI); + beforeEach(function () { + client = this.configuration.newClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -37,8 +44,8 @@ describe('OIDC Auth Spec Prose Tests', function () { }); describe('3.2 Allowed Hosts Ignored', function () { - before(function () { - client = new MongoClient(process.env.MONGODB_URI, { + beforeEach(function () { + client = this.configuration.newClient(process.env.MONGODB_URI, { authMechanismProperties: { ALLOWED_HOSTS: [] } @@ -57,9 +64,9 @@ describe('OIDC Auth Spec Prose Tests', function () { }); describe('3.3 Main Cache Not Used', function () { - before(function () { + beforeEach(function () { callbackCache?.clear(); - client = new MongoClient(process.env.MONGODB_URI); + client = this.configuration.newClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -76,10 +83,10 @@ describe('OIDC Auth Spec Prose Tests', function () { }); describe('3.4 Azure Cache is Used', function () { - before(function () { + beforeEach(function () { callbackCache?.clear(); azureCache?.clear(); - client = new MongoClient(process.env.MONGODB_URI); + client = this.configuration.newClient(process.env.MONGODB_URI); collection = client.db('test').collection('test'); }); @@ -148,9 +155,9 @@ describe('OIDC Auth Spec Prose Tests', function () { }); }; - before(async function () { + beforeEach(async function () { azureCache?.clear(); - client = new MongoClient(process.env.MONGODB_URI, { monitorCommands: true }); + client = this.configuration.newClient(process.env.MONGODB_URI, { monitorCommands: true }); await client.db('test').collection('test').findOne(); addListeners(); await setupFailPoint(); diff --git a/test/tools/runner/config.ts b/test/tools/runner/config.ts index c7719bb103e..dae6f7a4d87 100644 --- a/test/tools/runner/config.ts +++ b/test/tools/runner/config.ts @@ -153,6 +153,10 @@ export class TestConfiguration { return this.options.replicaSet; } + isAzureOIDC(uri: string): boolean { + return uri.indexOf('MONGODB-OIDC') > -1 && uri.indexOf('PROVIDER_NAME:azure') > -1; + } + newClient(dbOptions?: string | Record, serverOptions?: Record) { serverOptions = Object.assign({}, getEnvironmentalOptions(), serverOptions); From 506e6b64236db3ec4ca41bc01999f7d5ce4ddce5 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 13:24:16 -0400 Subject: [PATCH 34/40] test: fix prose test --- test/integration/auth/mongodb_oidc_azure.prose.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts index 910a44d888d..448f4a9fc6d 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -19,7 +19,7 @@ describe('OIDC Auth Spec Prose Tests', function () { beforeEach(function () { if (!this.configuration.isAzureOIDC(process.env.MONGODB_URI)) { - this.currentTest?.skipReason = 'Azure OIDC prose tests require an Azure OIDC environment.'; + this.skipReason = 'Azure OIDC prose tests require an Azure OIDC environment.'; this.skip(); } }); From 29ed9016187afd66e9c65e8b4305a2925f24c820 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 13:49:43 -0400 Subject: [PATCH 35/40] test: fix ubuntu distro --- .evergreen/ci_matrix_constants.js | 2 +- .evergreen/config.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.evergreen/ci_matrix_constants.js b/.evergreen/ci_matrix_constants.js index ec3ca578659..8e9578c9fe3 100644 --- a/.evergreen/ci_matrix_constants.js +++ b/.evergreen/ci_matrix_constants.js @@ -17,7 +17,7 @@ const DEFAULT_OS = 'rhel80-large'; const WINDOWS_OS = 'windows-vsCurrent-large'; const MACOS_OS = 'macos-1100'; const UBUNTU_OS = 'ubuntu1804-large'; -const UBUNTU_20_OS = 'ubuntu1004-small' +const UBUNTU_20_OS = 'ubuntu2004-small' const DEBIAN_OS = 'debian11-small'; module.exports = { diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 8b54523fee3..b331040e652 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -4042,7 +4042,7 @@ buildvariants: - test-azurekms-fail-task - name: ubuntu20-test-azure-oidc display_name: Azure OIDC - run_on: ubuntu1004-small + run_on: ubuntu2004-small batchtime: 20160 tasks: - testazureoidc_task_group From 23be5734c8d48699a29b6b664d5db423dfcefb8d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 14:37:32 -0400 Subject: [PATCH 36/40] test: run correct script --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index efcbc2f8700..e9ff32237b7 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", "check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts", - "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc_azure.prose.test.ts", + "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/mongodb_oidc_azure.prose.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js", From 236a64c49e9873f5309c396a9bc09d6f51986e41 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 14:49:25 -0400 Subject: [PATCH 37/40] test: use auth dir --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e9ff32237b7..fbd2613993c 100644 --- a/package.json +++ b/package.json @@ -129,7 +129,7 @@ "check:adl": "mocha --config test/mocha_mongodb.json test/manual/atlas-data-lake-testing", "check:aws": "nyc mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_aws.test.ts", "check:oidc": "mocha --config test/mocha_mongodb.json test/manual/mongodb_oidc.prose.test.ts", - "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/mongodb_oidc_azure.prose.test.ts", + "check:oidc-azure": "mocha --config test/mocha_mongodb.json test/integration/auth/mongodb_oidc_azure.prose.test.ts", "check:ocsp": "mocha --config test/manual/mocharc.json test/manual/ocsp_support.test.js", "check:kerberos": "nyc mocha --config test/manual/mocharc.json test/manual/kerberos.test.ts", "check:tls": "mocha --config test/manual/mocharc.json test/manual/tls_support.test.js", From 384eaa19fdb72e0f8484b1edafd87ee0d8f03e95 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 30 May 2023 15:28:01 -0400 Subject: [PATCH 38/40] test: use subprocess --- .evergreen/config.in.yml | 20 ++++++++++---------- .evergreen/config.yml | 20 ++++++++++---------- .evergreen/run-oidc-tests-azure.sh | 11 +++++++++++ 3 files changed, 31 insertions(+), 20 deletions(-) create mode 100644 .evergreen/run-oidc-tests-azure.sh diff --git a/.evergreen/config.in.yml b/.evergreen/config.in.yml index 4027e96a603..dce18afede9 100644 --- a/.evergreen/config.in.yml +++ b/.evergreen/config.in.yml @@ -1247,17 +1247,17 @@ tasks: - name: "oidc-auth-test-azure-latest" commands: - func: "install dependencies" - - command: shell.exec + - command: subprocess.exec params: - shell: bash - script: |- - set -o errexit - ${PREPARE_SHELL} - cd src - 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" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + 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 task_groups: - name: serverless_task_group diff --git a/.evergreen/config.yml b/.evergreen/config.yml index b331040e652..e5aac2fa910 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -1171,17 +1171,17 @@ tasks: - name: oidc-auth-test-azure-latest commands: - func: install dependencies - - command: shell.exec + - command: subprocess.exec params: - shell: bash - script: |- - set -o errexit - ${PREPARE_SHELL} - cd src - 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" - bash $DRIVERS_TOOLS/.evergreen/auth_oidc/azure/run-driver-test.sh + 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-latest-server tags: - latest diff --git a/.evergreen/run-oidc-tests-azure.sh b/.evergreen/run-oidc-tests-azure.sh new file mode 100644 index 00000000000..6e65bff3f44 --- /dev/null +++ b/.evergreen/run-oidc-tests-azure.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -o xtrace # Write all commands first to stderr +set -o errexit # Exit the script with error if any of the commands fail + +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 From 0064fdd4bd7401d7e88d42bcffa5771542fa3394 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Fri, 2 Jun 2023 10:56:43 -0400 Subject: [PATCH 39/40] chore: update comment --- src/cmap/auth/mongodb_oidc/cache.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cmap/auth/mongodb_oidc/cache.ts b/src/cmap/auth/mongodb_oidc/cache.ts index 3e74adf1eff..e23685b3bca 100644 --- a/src/cmap/auth/mongodb_oidc/cache.ts +++ b/src/cmap/auth/mongodb_oidc/cache.ts @@ -22,7 +22,7 @@ export abstract class ExpiringCacheEntry { } /** - * Get an expiration time in milliseconds past epoch. Defaults to immediate. + * Get an expiration time in milliseconds past epoch. */ private expirationTime(expiresInSeconds: number): number { return Date.now() + expiresInSeconds * 1000; From d901e526798694a89489d652efe7c9bd0e856d54 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Tue, 6 Jun 2023 16:50:56 +0200 Subject: [PATCH 40/40] fix: lint --- test/integration/auth/mongodb_oidc_azure.prose.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/integration/auth/mongodb_oidc_azure.prose.test.ts b/test/integration/auth/mongodb_oidc_azure.prose.test.ts index 448f4a9fc6d..2dc95b4c935 100644 --- a/test/integration/auth/mongodb_oidc_azure.prose.test.ts +++ b/test/integration/auth/mongodb_oidc_azure.prose.test.ts @@ -1,11 +1,11 @@ import { expect } from 'chai'; import { - Collection, - CommandFailedEvent, - CommandStartedEvent, - CommandSucceededEvent, - MongoClient, + type Collection, + type CommandFailedEvent, + type CommandStartedEvent, + type CommandSucceededEvent, + type MongoClient, OIDC_WORKFLOWS } from '../../mongodb';