From e386c87cc7c4f582686b01b40436516fa4ef350d Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 19 May 2025 23:24:12 +0200 Subject: [PATCH 1/6] feat: clear oidc cache when pool clears --- src/cmap/auth/mongodb_oidc.ts | 2 ++ src/cmap/connection_pool.ts | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 4cab886112f..272beec7aa5 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -93,6 +93,8 @@ type EnvironmentName = 'test' | 'azure' | 'gcp' | 'k8s' | undefined; /** @internal */ export interface Workflow { + cache: TokenCache; + /** * All device workflows must implement this method in order to get the access * token and then call authenticate with it. diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 00321ec2342..0521cb71da3 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -61,6 +61,7 @@ import { WaitQueueTimeoutError } from './errors'; import { ConnectionPoolMetrics } from './metrics'; +import { MongoDBOIDC } from './auth/mongodb_oidc'; /** @public */ export interface ConnectionPoolOptions extends Omit { @@ -428,6 +429,19 @@ export class ConnectionPool extends TypedEventEmitter { return; } + // If we are clearing the connnection pool when using OIDC, we need to remove the access token + // from the cache so we dont' try to use the same token again for initial auth on a new connection + // when the token may have expired. + const clientState = this.server.topology.client.s; + const credentials = clientState.options.credentials; + if (credentials?.mechanism === 'MONGODB-OIDC') { + const provider = this.server.topology.client.s.authProviders.getOrCreateProvider( + credentials.mechanism, + credentials.mechanismProperties + ) as MongoDBOIDC; + provider.workflow.cache.removeAccessToken(); + } + // handle load balanced case if (this.loadBalanced) { const { serviceId } = options; From 0a0dd301770f8a417d15c98593b4f98a45545457 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 1 Jun 2025 15:49:52 -0400 Subject: [PATCH 2/6] fix: migrate machine workflows to callbacks --- src/cmap/auth/mongodb_oidc.ts | 19 ++- .../automated_callback_workflow.ts | 3 + .../mongodb_oidc/azure_machine_workflow.ts | 46 ++---- .../auth/mongodb_oidc/gcp_machine_workflow.ts | 35 ++--- .../auth/mongodb_oidc/k8s_machine_workflow.ts | 40 ++--- .../auth/mongodb_oidc/machine_workflow.ts | 142 ------------------ .../mongodb_oidc/token_machine_workflow.ts | 33 ++-- src/cmap/connection_pool.ts | 2 +- test/mongodb.ts | 3 - .../azure_machine_workflow.test.ts | 16 +- .../mongodb_oidc/gcp_machine_workflow.test.ts | 42 ++---- .../token_machine_workflow.test.ts | 12 +- 12 files changed, 90 insertions(+), 303 deletions(-) delete mode 100644 src/cmap/auth/mongodb_oidc/machine_workflow.ts diff --git a/src/cmap/auth/mongodb_oidc.ts b/src/cmap/auth/mongodb_oidc.ts index 272beec7aa5..231862d2c65 100644 --- a/src/cmap/auth/mongodb_oidc.ts +++ b/src/cmap/auth/mongodb_oidc.ts @@ -4,11 +4,12 @@ import type { HandshakeDocument } from '../connect'; import type { Connection } from '../connection'; import { type AuthContext, AuthProvider } from './auth_provider'; import type { MongoCredentials } from './mongo_credentials'; -import { AzureMachineWorkflow } from './mongodb_oidc/azure_machine_workflow'; -import { GCPMachineWorkflow } from './mongodb_oidc/gcp_machine_workflow'; -import { K8SMachineWorkflow } from './mongodb_oidc/k8s_machine_workflow'; +import { AutomatedCallbackWorkflow } from './mongodb_oidc/automated_callback_workflow'; +import { callback as azureCallback } from './mongodb_oidc/azure_machine_workflow'; +import { callback as gcpCallback } from './mongodb_oidc/gcp_machine_workflow'; +import { callback as k8sCallback } from './mongodb_oidc/k8s_machine_workflow'; import { TokenCache } from './mongodb_oidc/token_cache'; -import { TokenMachineWorkflow } from './mongodb_oidc/token_machine_workflow'; +import { callback as testCallback } from './mongodb_oidc/token_machine_workflow'; /** Error when credentials are missing. */ const MISSING_CREDENTIALS_ERROR = 'AuthContext must provide credentials.'; @@ -78,6 +79,8 @@ export interface OIDCCallbackParams { idpInfo?: IdPInfo; /** The refresh token, if applicable, to be used by the callback to request a new token from the issuer. */ refreshToken?: string; + /** The token audience for GCP and Azure. */ + tokenAudience?: string; } /** @@ -118,10 +121,10 @@ export interface Workflow { /** @internal */ export const OIDC_WORKFLOWS: Map Workflow> = new Map(); -OIDC_WORKFLOWS.set('test', () => new TokenMachineWorkflow(new TokenCache())); -OIDC_WORKFLOWS.set('azure', () => new AzureMachineWorkflow(new TokenCache())); -OIDC_WORKFLOWS.set('gcp', () => new GCPMachineWorkflow(new TokenCache())); -OIDC_WORKFLOWS.set('k8s', () => new K8SMachineWorkflow(new TokenCache())); +OIDC_WORKFLOWS.set('test', () => new AutomatedCallbackWorkflow(new TokenCache(), testCallback)); +OIDC_WORKFLOWS.set('azure', () => new AutomatedCallbackWorkflow(new TokenCache(), azureCallback)); +OIDC_WORKFLOWS.set('gcp', () => new AutomatedCallbackWorkflow(new TokenCache(), gcpCallback)); +OIDC_WORKFLOWS.set('k8s', () => new AutomatedCallbackWorkflow(new TokenCache(), k8sCallback)); /** * OIDC auth provider. diff --git a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts index f98d87f6a27..3ecb7c3f7d0 100644 --- a/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/automated_callback_workflow.ts @@ -66,6 +66,9 @@ export class AutomatedCallbackWorkflow extends CallbackWorkflow { if (credentials.username) { params.username = credentials.username; } + if (credentials.mechanismProperties.TOKEN_RESOURCE) { + params.tokenAudience = credentials.mechanismProperties.TOKEN_RESOURCE; + } const timeout = Timeout.expires(AUTOMATED_TIMEOUT_MS); try { return await Promise.race([this.executeAndValidateCallback(params), timeout]); diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 1f41b8dc08d..289e0d9bb8a 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,9 +1,7 @@ import { addAzureParams, AZURE_BASE_URL } from '../../../client-side-encryption/providers/azure'; import { MongoAzureError } from '../../../error'; import { get } from '../../../utils'; -import type { MongoCredentials } from '../mongo_credentials'; -import { type AccessToken, MachineWorkflow } from './machine_workflow'; -import { type TokenCache } from './token_cache'; +import type { OIDCCallbackParams, OIDCResponse } from '../mongodb_oidc'; /** Azure request headers. */ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); @@ -17,39 +15,27 @@ const TOKEN_RESOURCE_MISSING_ERROR = 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is azure.'; /** - * Device workflow implementation for Azure. - * - * @internal + * The callback function to be used in the automated callback workflow. + * @param params - The OIDC callback parameters. + * @returns The OIDC response. */ -export class AzureMachineWorkflow extends MachineWorkflow { - /** - * Instantiate the machine workflow. - */ - constructor(cache: TokenCache) { - super(cache); +export async function callback(params: OIDCCallbackParams): Promise { + const tokenAudience = params.tokenAudience; + const username = params.username; + if (!tokenAudience) { + throw new MongoAzureError(TOKEN_RESOURCE_MISSING_ERROR); } - - /** - * Get the token from the environment. - */ - async getToken(credentials?: MongoCredentials): Promise { - const tokenAudience = credentials?.mechanismProperties.TOKEN_RESOURCE; - const username = credentials?.username; - if (!tokenAudience) { - throw new MongoAzureError(TOKEN_RESOURCE_MISSING_ERROR); - } - const response = await getAzureTokenData(tokenAudience, username); - if (!isEndpointResultValid(response)) { - throw new MongoAzureError(ENDPOINT_RESULT_ERROR); - } - return response; + const response = await getAzureTokenData(tokenAudience, username); + if (!isEndpointResultValid(response)) { + throw new MongoAzureError(ENDPOINT_RESULT_ERROR); } + return response; } /** * Hit the Azure endpoint to get the token data. */ -async function getAzureTokenData(tokenAudience: string, username?: string): Promise { +async function getAzureTokenData(tokenAudience: string, username?: string): Promise { const url = new URL(AZURE_BASE_URL); addAzureParams(url, tokenAudience, username); const response = await get(url, { @@ -62,8 +48,8 @@ async function getAzureTokenData(tokenAudience: string, username?: string): Prom } const result = JSON.parse(response.body); return { - access_token: result.access_token, - expires_in: Number(result.expires_in) + accessToken: result.access_token, + expiresInSeconds: Number(result.expires_in) }; } diff --git a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts index 6b8c1ee0541..536b88de009 100644 --- a/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts @@ -1,8 +1,6 @@ import { MongoGCPError } from '../../../error'; import { get } from '../../../utils'; -import { type MongoCredentials } from '../mongo_credentials'; -import { type AccessToken, MachineWorkflow } from './machine_workflow'; -import { type TokenCache } from './token_cache'; +import type { OIDCCallbackParams, OIDCResponse } from '../mongodb_oidc'; /** GCP base URL. */ const GCP_BASE_URL = @@ -15,30 +13,23 @@ const GCP_HEADERS = Object.freeze({ 'Metadata-Flavor': 'Google' }); const TOKEN_RESOURCE_MISSING_ERROR = 'TOKEN_RESOURCE must be set in the auth mechanism properties when ENVIRONMENT is gcp.'; -export class GCPMachineWorkflow extends MachineWorkflow { - /** - * Instantiate the machine workflow. - */ - constructor(cache: TokenCache) { - super(cache); - } - - /** - * Get the token from the environment. - */ - async getToken(credentials?: MongoCredentials): Promise { - const tokenAudience = credentials?.mechanismProperties.TOKEN_RESOURCE; - if (!tokenAudience) { - throw new MongoGCPError(TOKEN_RESOURCE_MISSING_ERROR); - } - return await getGcpTokenData(tokenAudience); +/** + * The callback function to be used in the automated callback workflow. + * @param params - The OIDC callback parameters. + * @returns The OIDC response. + */ +export async function callback(params: OIDCCallbackParams): Promise { + const tokenAudience = params.tokenAudience; + if (!tokenAudience) { + throw new MongoGCPError(TOKEN_RESOURCE_MISSING_ERROR); } + return await getGcpTokenData(tokenAudience); } /** * Hit the GCP endpoint to get the token data. */ -async function getGcpTokenData(tokenAudience: string): Promise { +async function getGcpTokenData(tokenAudience: string): Promise { const url = new URL(GCP_BASE_URL); url.searchParams.append('audience', tokenAudience); const response = await get(url, { @@ -49,5 +40,5 @@ async function getGcpTokenData(tokenAudience: string): Promise { `Status code ${response.status} returned from the GCP endpoint. Response body: ${response.body}` ); } - return { access_token: response.body }; + return { accessToken: response.body }; } diff --git a/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts index 22dc9cb9f62..4eea3173170 100644 --- a/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts @@ -1,7 +1,6 @@ import { readFile } from 'fs/promises'; -import { type AccessToken, MachineWorkflow } from './machine_workflow'; -import { type TokenCache } from './token_cache'; +import type { OIDCResponse } from '../mongodb_oidc'; /** The fallback file name */ const FALLBACK_FILENAME = '/var/run/secrets/kubernetes.io/serviceaccount/token'; @@ -12,27 +11,20 @@ const AZURE_FILENAME = 'AZURE_FEDERATED_TOKEN_FILE'; /** The AWS environment variable for the file name. */ const AWS_FILENAME = 'AWS_WEB_IDENTITY_TOKEN_FILE'; -export class K8SMachineWorkflow extends MachineWorkflow { - /** - * Instantiate the machine workflow. - */ - constructor(cache: TokenCache) { - super(cache); - } - - /** - * Get the token from the environment. - */ - async getToken(): Promise { - let filename: string; - if (process.env[AZURE_FILENAME]) { - filename = process.env[AZURE_FILENAME]; - } else if (process.env[AWS_FILENAME]) { - filename = process.env[AWS_FILENAME]; - } else { - filename = FALLBACK_FILENAME; - } - const token = await readFile(filename, 'utf8'); - return { access_token: token }; +/** + * The callback function to be used in the automated callback workflow. + * @param params - The OIDC callback parameters. + * @returns The OIDC response. + */ +export async function callback(): Promise { + let filename: string; + if (process.env[AZURE_FILENAME]) { + filename = process.env[AZURE_FILENAME]; + } else if (process.env[AWS_FILENAME]) { + filename = process.env[AWS_FILENAME]; + } else { + filename = FALLBACK_FILENAME; } + const token = await readFile(filename, 'utf8'); + return { accessToken: token }; } diff --git a/src/cmap/auth/mongodb_oidc/machine_workflow.ts b/src/cmap/auth/mongodb_oidc/machine_workflow.ts deleted file mode 100644 index b666335ec0c..00000000000 --- a/src/cmap/auth/mongodb_oidc/machine_workflow.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { setTimeout } from 'timers/promises'; - -import { type Document } from '../../../bson'; -import { ns } from '../../../utils'; -import type { Connection } from '../../connection'; -import type { MongoCredentials } from '../mongo_credentials'; -import type { Workflow } from '../mongodb_oidc'; -import { finishCommandDocument } from './command_builders'; -import { type TokenCache } from './token_cache'; - -/** The time to throttle callback calls. */ -const THROTTLE_MS = 100; - -/** - * The access token format. - * @internal - */ -export interface AccessToken { - access_token: string; - expires_in?: number; -} - -/** @internal */ -export type OIDCTokenFunction = (credentials: MongoCredentials) => Promise; - -/** - * Common behaviour for OIDC machine workflows. - * @internal - */ -export abstract class MachineWorkflow implements Workflow { - cache: TokenCache; - callback: OIDCTokenFunction; - lastExecutionTime: number; - - /** - * Instantiate the machine workflow. - */ - constructor(cache: TokenCache) { - this.cache = cache; - this.callback = this.withLock(this.getToken.bind(this)); - this.lastExecutionTime = Date.now() - THROTTLE_MS; - } - - /** - * Execute the workflow. Gets the token from the subclass implementation. - */ - async execute(connection: Connection, credentials: MongoCredentials): Promise { - const token = await this.getTokenFromCacheOrEnv(connection, credentials); - const command = finishCommandDocument(token); - await connection.command(ns(credentials.source), command, undefined); - } - - /** - * Reauthenticate on a machine workflow just grabs the token again since the server - * has said the current access token is invalid or expired. - */ - async reauthenticate(connection: Connection, credentials: MongoCredentials): Promise { - if (this.cache.hasAccessToken) { - // Reauthentication implies the token has expired. - if (connection.accessToken === this.cache.getAccessToken()) { - // If connection's access token is the same as the cache's, remove - // the token from the cache and connection. - this.cache.removeAccessToken(); - delete connection.accessToken; - } else { - // If the connection's access token is different from the cache's, set - // the cache's token on the connection and do not remove from the - // cache. - connection.accessToken = this.cache.getAccessToken(); - } - } - await this.execute(connection, credentials); - } - - /** - * Get the document to add for speculative authentication. - */ - async speculativeAuth(connection: Connection, credentials: MongoCredentials): Promise { - // The spec states only cached access tokens can use speculative auth. - if (!this.cache.hasAccessToken) { - return {}; - } - const token = await this.getTokenFromCacheOrEnv(connection, credentials); - const document = finishCommandDocument(token); - document.db = credentials.source; - return { speculativeAuthenticate: document }; - } - - /** - * Get the token from the cache or environment. - */ - private async getTokenFromCacheOrEnv( - connection: Connection, - credentials: MongoCredentials - ): Promise { - if (this.cache.hasAccessToken) { - const token = this.cache.getAccessToken(); - // New connections won't have an access token so ensure we set here. - if (!connection.accessToken) { - connection.accessToken = token; - } - return token; - } else { - const token = await this.callback(credentials); - this.cache.put({ accessToken: token.access_token, expiresInSeconds: token.expires_in }); - // Put the access token on the connection as well. - connection.accessToken = token.access_token; - return token.access_token; - } - } - - /** - * Ensure the callback is only executed one at a time, and throttled to - * only once per 100ms. - */ - private withLock(callback: OIDCTokenFunction): OIDCTokenFunction { - let lock: Promise = Promise.resolve(); - return async (credentials: MongoCredentials): Promise => { - // We do this to ensure that we would never return the result of the - // previous lock, only the current callback's value would get returned. - await lock; - lock = lock - - .catch(() => null) - - .then(async () => { - const difference = Date.now() - this.lastExecutionTime; - if (difference <= THROTTLE_MS) { - await setTimeout(THROTTLE_MS - difference); - } - this.lastExecutionTime = Date.now(); - return await callback(credentials); - }); - return await lock; - }; - } - - /** - * Get the token from the environment or endpoint. - */ - abstract getToken(credentials: MongoCredentials): Promise; -} diff --git a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts index de32c469594..3a6166d67b4 100644 --- a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts @@ -1,34 +1,21 @@ import * as fs from 'fs'; import { MongoAWSError } from '../../../error'; -import { type AccessToken, MachineWorkflow } from './machine_workflow'; -import { type TokenCache } from './token_cache'; +import { type OIDCResponse } from '../mongodb_oidc'; /** Error for when the token is missing in the environment. */ const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; /** - * Device workflow implementation for AWS. - * - * @internal + * The callback function to be used in the automated callback workflow. + * @param params - The OIDC callback parameters. + * @returns The OIDC response. */ -export class TokenMachineWorkflow extends MachineWorkflow { - /** - * Instantiate the machine workflow. - */ - constructor(cache: TokenCache) { - super(cache); - } - - /** - * Get the token from the environment. - */ - async getToken(): Promise { - const tokenFile = process.env.OIDC_TOKEN_FILE; - if (!tokenFile) { - throw new MongoAWSError(TOKEN_MISSING_ERROR); - } - const token = await fs.promises.readFile(tokenFile, 'utf8'); - return { access_token: token }; +export async function callback(): Promise { + const tokenFile = process.env.OIDC_TOKEN_FILE; + if (!tokenFile) { + throw new MongoAWSError(TOKEN_MISSING_ERROR); } + const token = await fs.promises.readFile(tokenFile, 'utf8'); + return { accessToken: token }; } diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index 0521cb71da3..caf1d0b1b23 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -39,6 +39,7 @@ import { now, promiseWithResolvers } from '../utils'; +import type { MongoDBOIDC } from './auth/mongodb_oidc'; import { connect } from './connect'; import { Connection, type ConnectionEvents, type ConnectionOptions } from './connection'; import { @@ -61,7 +62,6 @@ import { WaitQueueTimeoutError } from './errors'; import { ConnectionPoolMetrics } from './metrics'; -import { MongoDBOIDC } from './auth/mongodb_oidc'; /** @public */ export interface ConnectionPoolOptions extends Omit { diff --git a/test/mongodb.ts b/test/mongodb.ts index f94a511929c..d9ffc4c0a11 100644 --- a/test/mongodb.ts +++ b/test/mongodb.ts @@ -121,9 +121,6 @@ export * from '../src/cmap/auth/mongodb_aws'; export * from '../src/cmap/auth/mongodb_oidc'; export * from '../src/cmap/auth/mongodb_oidc/azure_machine_workflow'; export * from '../src/cmap/auth/mongodb_oidc/callback_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/gcp_machine_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/machine_workflow'; -export * from '../src/cmap/auth/mongodb_oidc/token_machine_workflow'; export * from '../src/cmap/auth/plain'; export * from '../src/cmap/auth/providers'; export * from '../src/cmap/auth/scram'; diff --git a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts index b60c4f045da..873b06ee003 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -1,20 +1,20 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; -import { AzureMachineWorkflow, Connection, MongoCredentials } from '../../../../mongodb'; +import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/azure_machine_workflow'; +import { OIDC_VERSION, type OIDCCallbackParams } from '../../../../mongodb'; describe('AzureMachineFlow', function () { describe('#execute', function () { - const workflow = new AzureMachineWorkflow(new TokenCache()); - context('when TOKEN_RESOURCE is not set', function () { - const connection = sinon.createStubInstance(Connection); - const credentials = sinon.createStubInstance(MongoCredentials); + const controller = new AbortController(); + const params: OIDCCallbackParams = { + timeoutContext: controller.signal, + version: OIDC_VERSION + }; it('throws an error', async function () { - const error = await workflow.execute(connection, credentials).catch(error => error); + const error = await callback(params).catch(error => error); expect(error.message).to.include('TOKEN_RESOURCE'); }); }); diff --git a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts index 48d66f49d82..e8546900287 100644 --- a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -1,46 +1,22 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; -import { Connection, GCPMachineWorkflow, MongoCredentials } from '../../../../mongodb'; +import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/gcp_machine_workflow'; +import { OIDC_VERSION, type OIDCCallbackParams } from '../../../../mongodb'; describe('GCPMachineFlow', function () { - describe('#execute', function () { - const workflow = new GCPMachineWorkflow(new TokenCache()); - + describe('#callback', function () { context('when TOKEN_RESOURCE is not set', function () { - const connection = sinon.createStubInstance(Connection); - const credentials = sinon.createStubInstance(MongoCredentials); + const controller = new AbortController(); + const params: OIDCCallbackParams = { + timeoutContext: controller.signal, + version: OIDC_VERSION + }; it('throws an error', async function () { - const error = await workflow.execute(connection, credentials).catch(error => error); + const error = await callback(params).catch(error => error); expect(error.message).to.include('TOKEN_RESOURCE'); }); }); }); - - describe('#getTokenFromCacheOrEnv', function () { - context('when the cache has a token', function () { - const connection = sinon.createStubInstance(Connection); - const credentials = sinon.createStubInstance(MongoCredentials); - - context('when the connection has no token', function () { - let cache; - let workflow; - - this.beforeEach(function () { - cache = new TokenCache(); - cache.put({ accessToken: 'test', expiresInSeconds: 7200 }); - workflow = new GCPMachineWorkflow(cache); - }); - - it('sets the token on the connection', async function () { - const token = await workflow.getTokenFromCacheOrEnv(connection, credentials); - expect(token).to.equal('test'); - expect(connection.accessToken).to.equal('test'); - }); - }); - }); - }); }); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts index b0302d7f03e..7bcdf449a43 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -1,18 +1,12 @@ import { expect } from 'chai'; -import * as sinon from 'sinon'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports -import { TokenCache } from '../../../../../src/cmap/auth/mongodb_oidc/token_cache'; -import { Connection, MongoCredentials, TokenMachineWorkflow } from '../../../../mongodb'; +import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/token_machine_workflow'; describe('TokenMachineFlow', function () { - describe('#execute', function () { - const workflow = new TokenMachineWorkflow(new TokenCache()); - + describe('#callback', function () { context('when OIDC_TOKEN_FILE is not in the env', function () { let file; - const connection = sinon.createStubInstance(Connection); - const credentials = sinon.createStubInstance(MongoCredentials); before(function () { file = process.env.OIDC_TOKEN_FILE; @@ -26,7 +20,7 @@ describe('TokenMachineFlow', function () { }); it('throws an error', async function () { - const error = await workflow.execute(connection, credentials).catch(error => error); + const error = await callback().catch(error => error); expect(error.message).to.include('OIDC_TOKEN_FILE'); }); }); From c41b42bb5b8a76e54d92d63be252cfcd0d2730cf Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 1 Jun 2025 15:52:27 -0400 Subject: [PATCH 3/6] chore: revert pool clear clearing cache --- src/cmap/connection_pool.ts | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/cmap/connection_pool.ts b/src/cmap/connection_pool.ts index caf1d0b1b23..00321ec2342 100644 --- a/src/cmap/connection_pool.ts +++ b/src/cmap/connection_pool.ts @@ -39,7 +39,6 @@ import { now, promiseWithResolvers } from '../utils'; -import type { MongoDBOIDC } from './auth/mongodb_oidc'; import { connect } from './connect'; import { Connection, type ConnectionEvents, type ConnectionOptions } from './connection'; import { @@ -429,19 +428,6 @@ export class ConnectionPool extends TypedEventEmitter { return; } - // If we are clearing the connnection pool when using OIDC, we need to remove the access token - // from the cache so we dont' try to use the same token again for initial auth on a new connection - // when the token may have expired. - const clientState = this.server.topology.client.s; - const credentials = clientState.options.credentials; - if (credentials?.mechanism === 'MONGODB-OIDC') { - const provider = this.server.topology.client.s.authProviders.getOrCreateProvider( - credentials.mechanism, - credentials.mechanismProperties - ) as MongoDBOIDC; - provider.workflow.cache.removeAccessToken(); - } - // handle load balanced case if (this.loadBalanced) { const { serviceId } = options; From 5b7a8068a13c53d2136d387b1463f3288f3799e2 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Sun, 1 Jun 2025 15:54:46 -0400 Subject: [PATCH 4/6] test: fix test names --- .../cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts | 4 ++-- test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts | 2 +- .../cmap/auth/mongodb_oidc/token_machine_workflow.test.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts index 873b06ee003..b61bbd62465 100644 --- a/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/azure_machine_workflow.test.ts @@ -4,8 +4,8 @@ import { expect } from 'chai'; import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/azure_machine_workflow'; import { OIDC_VERSION, type OIDCCallbackParams } from '../../../../mongodb'; -describe('AzureMachineFlow', function () { - describe('#execute', function () { +describe('Azure machine workflow', function () { + describe('#callback', function () { context('when TOKEN_RESOURCE is not set', function () { const controller = new AbortController(); const params: OIDCCallbackParams = { diff --git a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts index e8546900287..f99465cb1f7 100644 --- a/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/gcp_machine_workflow.test.ts @@ -4,7 +4,7 @@ import { expect } from 'chai'; import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/gcp_machine_workflow'; import { OIDC_VERSION, type OIDCCallbackParams } from '../../../../mongodb'; -describe('GCPMachineFlow', function () { +describe('GCP machine workflow', function () { describe('#callback', function () { context('when TOKEN_RESOURCE is not set', function () { const controller = new AbortController(); diff --git a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts index 7bcdf449a43..624c37dbb27 100644 --- a/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts +++ b/test/unit/cmap/auth/mongodb_oidc/token_machine_workflow.test.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { callback } from '../../../../../src/cmap/auth/mongodb_oidc/token_machine_workflow'; -describe('TokenMachineFlow', function () { +describe('Token machine workflow', function () { describe('#callback', function () { context('when OIDC_TOKEN_FILE is not in the env', function () { let file; From 580207365af916b16f880accc6ad4b78ca7fba1c Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 2 Jun 2025 09:46:08 -0400 Subject: [PATCH 5/6] test: fix azure test --- src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 289e0d9bb8a..806fb44d585 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -63,9 +63,9 @@ function isEndpointResultValid( ): token is { access_token: unknown; expires_in: unknown } { if (token == null || typeof token !== 'object') return false; return ( - 'access_token' in token && - typeof token.access_token === 'string' && - 'expires_in' in token && - typeof token.expires_in === 'number' + 'accessToken' in token && + typeof token.accessToken === 'string' && + 'expiresInSeconds' in token && + typeof token.expiresInSeconds === 'number' ); } From 36a1cfdae39cb455153b34ed3c23c693d134f8a8 Mon Sep 17 00:00:00 2001 From: Durran Jordan Date: Mon, 2 Jun 2025 11:57:34 -0400 Subject: [PATCH 6/6] chore: comments --- src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts | 8 +++++--- src/cmap/auth/mongodb_oidc/gcp_machine_workflow.ts | 8 +++++--- src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts | 6 +++--- src/cmap/auth/mongodb_oidc/token_machine_workflow.ts | 6 +++--- 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts index 806fb44d585..5331fea6ed1 100644 --- a/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/azure_machine_workflow.ts @@ -1,7 +1,7 @@ import { addAzureParams, AZURE_BASE_URL } from '../../../client-side-encryption/providers/azure'; import { MongoAzureError } from '../../../error'; import { get } from '../../../utils'; -import type { OIDCCallbackParams, OIDCResponse } from '../mongodb_oidc'; +import type { OIDCCallbackFunction, OIDCCallbackParams, OIDCResponse } from '../mongodb_oidc'; /** Azure request headers. */ const AZURE_HEADERS = Object.freeze({ Metadata: 'true', Accept: 'application/json' }); @@ -19,7 +19,9 @@ const TOKEN_RESOURCE_MISSING_ERROR = * @param params - The OIDC callback parameters. * @returns The OIDC response. */ -export async function callback(params: OIDCCallbackParams): Promise { +export const callback: OIDCCallbackFunction = async ( + params: OIDCCallbackParams +): Promise => { const tokenAudience = params.tokenAudience; const username = params.username; if (!tokenAudience) { @@ -30,7 +32,7 @@ export async function callback(params: OIDCCallbackParams): Promise { +export const callback: OIDCCallbackFunction = async ( + params: OIDCCallbackParams +): Promise => { const tokenAudience = params.tokenAudience; if (!tokenAudience) { throw new MongoGCPError(TOKEN_RESOURCE_MISSING_ERROR); } return await getGcpTokenData(tokenAudience); -} +}; /** * Hit the GCP endpoint to get the token data. diff --git a/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts index 4eea3173170..1df15926b10 100644 --- a/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/k8s_machine_workflow.ts @@ -1,6 +1,6 @@ import { readFile } from 'fs/promises'; -import type { OIDCResponse } from '../mongodb_oidc'; +import type { OIDCCallbackFunction, OIDCResponse } from '../mongodb_oidc'; /** The fallback file name */ const FALLBACK_FILENAME = '/var/run/secrets/kubernetes.io/serviceaccount/token'; @@ -16,7 +16,7 @@ const AWS_FILENAME = 'AWS_WEB_IDENTITY_TOKEN_FILE'; * @param params - The OIDC callback parameters. * @returns The OIDC response. */ -export async function callback(): Promise { +export const callback: OIDCCallbackFunction = async (): Promise => { let filename: string; if (process.env[AZURE_FILENAME]) { filename = process.env[AZURE_FILENAME]; @@ -27,4 +27,4 @@ export async function callback(): Promise { } const token = await readFile(filename, 'utf8'); return { accessToken: token }; -} +}; diff --git a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts index 3a6166d67b4..340be227453 100644 --- a/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts +++ b/src/cmap/auth/mongodb_oidc/token_machine_workflow.ts @@ -1,7 +1,7 @@ import * as fs from 'fs'; import { MongoAWSError } from '../../../error'; -import { type OIDCResponse } from '../mongodb_oidc'; +import type { OIDCCallbackFunction, OIDCResponse } from '../mongodb_oidc'; /** Error for when the token is missing in the environment. */ const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; @@ -11,11 +11,11 @@ const TOKEN_MISSING_ERROR = 'OIDC_TOKEN_FILE must be set in the environment.'; * @param params - The OIDC callback parameters. * @returns The OIDC response. */ -export async function callback(): Promise { +export const callback: OIDCCallbackFunction = async (): Promise => { const tokenFile = process.env.OIDC_TOKEN_FILE; if (!tokenFile) { throw new MongoAWSError(TOKEN_MISSING_ERROR); } const token = await fs.promises.readFile(tokenFile, 'utf8'); return { accessToken: token }; -} +};