From 033cd7bec0c39a6af5f10a891715bbf2e58e6ebc Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Tue, 4 Nov 2025 17:12:12 -0500 Subject: [PATCH 1/5] feat(oidc-mock-provider,mongodb-runner): make OIDC mocks more broadly usable COMPASS-10034 Internal TSEs have requested making oidc-mock-provider available for internal testing with OIDC. While it cannot replicate every aspect of real-world identity providers, it is an easily spun up local equivalent of those, and provides flexibility that those real-world identity providers lack in terms of configurability. This change widens the array of CLI options provided for the oidc-mock-provider CLI, and integrates it into mongodb-runner so that the latter can spin up a joint OIDC-IdP-and-mongod-cluster environment on Linux, if that is desired. --- packages/mongodb-runner/README.md | 1 + packages/mongodb-runner/package.json | 3 +- packages/mongodb-runner/src/cli.ts | 53 +++----- packages/mongodb-runner/src/index.ts | 2 +- packages/mongodb-runner/src/mongocluster.ts | 40 +++++- packages/mongodb-runner/src/oidc.ts | 123 ++++++++++++++++++ packages/mongodb-runner/src/runner-helpers.ts | 42 ++++++ packages/oidc-mock-provider/README.md | 65 ++++++++- .../bin/oidc-mock-provider.js | 61 +-------- packages/oidc-mock-provider/src/cli-parser.ts | 101 ++++++++++++++ packages/oidc-mock-provider/src/cli.ts | 24 ++++ packages/oidc-mock-provider/src/index.ts | 35 +++-- 12 files changed, 441 insertions(+), 109 deletions(-) create mode 100644 packages/mongodb-runner/src/oidc.ts mode change 100755 => 100644 packages/oidc-mock-provider/bin/oidc-mock-provider.js create mode 100644 packages/oidc-mock-provider/src/cli-parser.ts create mode 100755 packages/oidc-mock-provider/src/cli.ts diff --git a/packages/mongodb-runner/README.md b/packages/mongodb-runner/README.md index 03954e19..48ab3d37 100644 --- a/packages/mongodb-runner/README.md +++ b/packages/mongodb-runner/README.md @@ -11,6 +11,7 @@ Helper for spinning up MongoDB servers and clusters for testing. $ npx mongodb-runner start -t sharded $ npx mongodb-runner start -t replset -- --port 27017 $ npx mongodb-runner start -t replset -- --setParameter allowDiskUseByDefault=true +$ npx mongodb-runner start -t replset --version 8.2.x-enterprise --oidc='--payload={"groups":["x"],"sub":"y","aud":"aud"} --expiry=60 --skip-refresh-token' $ npx mongodb-runner stop --all $ npx mongodb-runner exec -t standalone -- sh -c 'mongosh $MONGODB_URI' $ npx mongodb-runner exec -t standalone -- --setParameter allowDiskUseByDefault=true -- sh -c 'mongosh $MONGODB_URI' diff --git a/packages/mongodb-runner/package.json b/packages/mongodb-runner/package.json index 4078d2cc..f68448ac 100644 --- a/packages/mongodb-runner/package.json +++ b/packages/mongodb-runner/package.json @@ -57,9 +57,10 @@ }, "dependencies": { "@mongodb-js/mongodb-downloader": "^1.0.0", + "@mongodb-js/oidc-mock-provider": "^0.11.5", + "@mongodb-js/saslprep": "^1.3.2", "debug": "^4.4.0", "mongodb": "^6.9.0", - "@mongodb-js/saslprep": "^1.3.2", "mongodb-connection-string-url": "^3.0.0", "yargs": "^17.7.2" }, diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index 6fdf2c6f..861a4acd 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -1,11 +1,8 @@ /* eslint-disable no-console */ import yargs from 'yargs'; -import { MongoCluster } from './mongocluster'; import os from 'os'; import path from 'path'; -import { spawn } from 'child_process'; import createDebug from 'debug'; -import { once } from 'events'; import * as utilities from './index'; (async function () { @@ -71,6 +68,10 @@ import * as utilities from './index'; type: 'boolean', describe: 'for `stop`: stop all clusters', }) + .option('oidc', { + type: 'string', + describe: 'Configure OIDC authentication on the server', + }) .option('debug', { type: 'boolean', describe: 'Enable debug output' }) .command('start', 'Start a MongoDB instance') .command('stop', 'Stop a MongoDB instance') @@ -87,9 +88,23 @@ import * as utilities from './index'; createDebug.enable('mongodb-runner'); } + if (argv.oidc && process.platform !== 'linux') { + console.warn( + 'OIDC authentication is currently only supported on Linux platforms.', + ); + } + if (argv.oidc && !argv.version?.includes('enterprise')) { + console.warn( + 'OIDC authentication is currently only supported on Enterprise server versions.', + ); + } + async function start() { const { cluster, id } = await utilities.start(argv, args); console.log(`Server started and running at ${cluster.connectionString}`); + if (cluster.oidcIssuer) { + console.log(`OIDC provider started and running at ${cluster.oidcIssuer}`); + } console.log('Run the following command to stop the instance:'); console.log( `${argv.$0} stop --id=${id}` + @@ -118,37 +133,7 @@ import * as utilities from './index'; } async function exec() { - let mongodArgs: string[]; - let execArgs: string[]; - - const doubleDashIndex = args.indexOf('--'); - if (doubleDashIndex !== -1) { - mongodArgs = args.slice(0, doubleDashIndex); - execArgs = args.slice(doubleDashIndex + 1); - } else { - mongodArgs = []; - execArgs = args; - } - const cluster = await MongoCluster.start({ - ...argv, - args: mongodArgs, - }); - try { - const [prog, ...progArgs] = execArgs; - const child = spawn(prog, progArgs, { - stdio: 'inherit', - env: { - ...process.env, - // both spellings since otherwise I'd end up misspelling these half of the time - MONGODB_URI: cluster.connectionString, - MONGODB_URL: cluster.connectionString, - MONGODB_HOSTPORT: cluster.hostport, - }, - }); - [process.exitCode] = await once(child, 'exit'); - } finally { - await cluster.close(); - } + await utilities.exec(argv, args); } // eslint-disable-next-line @typescript-eslint/require-await diff --git a/packages/mongodb-runner/src/index.ts b/packages/mongodb-runner/src/index.ts index 338de0c8..03290309 100644 --- a/packages/mongodb-runner/src/index.ts +++ b/packages/mongodb-runner/src/index.ts @@ -2,4 +2,4 @@ export { MongoServer, MongoServerOptions } from './mongoserver'; export { MongoCluster, MongoClusterOptions } from './mongocluster'; export type { ConnectionString } from 'mongodb-connection-string-url'; -export { prune, start, stop, instances } from './runner-helpers'; +export { prune, start, stop, exec, instances } from './runner-helpers'; diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 23ae02b2..8437b70d 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -6,6 +6,7 @@ import { downloadMongoDb } from '@mongodb-js/mongodb-downloader'; import type { MongoClientOptions } from 'mongodb'; import { MongoClient } from 'mongodb'; import { sleep, range, uuid, debug } from './util'; +import { OIDCMockProviderProcess } from './oidc'; export interface MongoClusterOptions extends Pick< @@ -19,6 +20,7 @@ export interface MongoClusterOptions version?: string; downloadDir?: string; downloadOptions?: DownloadOptions; + oidc?: string; } export class MongoCluster { @@ -26,6 +28,7 @@ export class MongoCluster { private replSetName?: string; private servers: MongoServer[] = []; // mongod/mongos private shards: MongoCluster[] = []; // replsets + private oidcMockProviderProcess?: OIDCMockProviderProcess; private constructor() { /* see .start() */ @@ -50,6 +53,7 @@ export class MongoCluster { replSetName: this.replSetName, servers: this.servers.map((srv) => srv.serialize()), shards: this.shards.map((shard) => shard.serialize()), + oidcMockProviderProcess: this.oidcMockProviderProcess?.serialize(), }; } @@ -67,6 +71,9 @@ export class MongoCluster { cluster.shards = await Promise.all( serialized.shards.map((shard: any) => MongoCluster.deserialize(shard)), ); + cluster.oidcMockProviderProcess = serialized.oidcMockProviderProcess + ? OIDCMockProviderProcess.deserialize(serialized.oidcMockProviderProcess) + : undefined; return cluster; } @@ -80,6 +87,10 @@ export class MongoCluster { }`; } + get oidcIssuer(): string | undefined { + return this.oidcMockProviderProcess?.issuer; + } + get connectionStringUrl(): ConnectionString { return new ConnectionString(this.connectionString); } @@ -105,6 +116,31 @@ export class MongoCluster { ); } + if (options.oidc !== undefined) { + cluster.oidcMockProviderProcess = await OIDCMockProviderProcess.start( + options.oidc || '--port=0', + ); + const oidcServerConfig = [ + { + issuer: cluster.oidcMockProviderProcess.issuer, + audience: cluster.oidcMockProviderProcess.audience, + authNamePrefix: 'dev', + clientId: 'cid', + authorizationClaim: 'groups', + }, + ]; + delete options.oidc; + options.args = [ + ...(options.args ?? []), + '--setParameter', + `oidcIdentityProviders=${JSON.stringify(oidcServerConfig)}`, + '--setParameter', + 'authenticationMechanisms=SCRAM-SHA-256,MONGODB-OIDC', + '--setParameter', + 'enableTestCommands=true', + ]; + } + if (options.topology === 'standalone') { cluster.servers.push( await MongoServer.start({ @@ -233,7 +269,9 @@ export class MongoCluster { async close(): Promise { await Promise.all( - [...this.servers, ...this.shards].map((closable) => closable.close()), + [...this.servers, ...this.shards, this.oidcMockProviderProcess].map( + (closable) => closable?.close(), + ), ); this.servers = []; this.shards = []; diff --git a/packages/mongodb-runner/src/oidc.ts b/packages/mongodb-runner/src/oidc.ts new file mode 100644 index 00000000..b4f79086 --- /dev/null +++ b/packages/mongodb-runner/src/oidc.ts @@ -0,0 +1,123 @@ +import { spawn } from 'child_process'; +import { once } from 'events'; +import { parseCLIArgs, OIDCMockProvider } from '@mongodb-js/oidc-mock-provider'; +import { debug } from './util'; + +if (process.env.RUN_OIDC_MOCK_PROVIDER !== undefined) { + (async function main() { + const uuid = crypto.randomUUID(); + debug('starting OIDC mock provider with UUID', uuid); + const config = parseCLIArgs(process.env.RUN_OIDC_MOCK_PROVIDER); + const sampleTokenConfig = await config.getTokenPayload({ + client_id: 'cid', + scope: 'scope', + }); + debug('sample OIDC token config', sampleTokenConfig, uuid); + const audience = sampleTokenConfig.payload.aud; + const provider = await OIDCMockProvider.create({ + ...config, + overrideRequestHandler(url, req, res) { + if (new URL(url).pathname === `/shutdown/${uuid}`) { + res.on('close', () => { + process.exit(); + }); + res.writeHead(200); + res.end(); + } + }, + }); + debug('started OIDC mock provider with UUID', { + issuer: provider.issuer, + uuid, + audience, + }); + process.send?.({ + issuer: provider.issuer, + uuid, + audience, + }); + })().catch((error) => { + // eslint-disable-next-line no-console + console.error('Error starting OIDC mock identity provider server:', error); + process.exitCode = 1; + }); +} + +export class OIDCMockProviderProcess { + pid?: number; + issuer?: string; + uuid?: string; + audience?: string; + + serialize(): unknown /* JSON-serializable */ { + return { + pid: this.pid, + issuer: this.issuer, + uuid: this.uuid, + audience: this.audience, + }; + } + + static deserialize(serialized: any): OIDCMockProviderProcess { + const process = new OIDCMockProviderProcess(); + process.pid = serialized.pid; + process.issuer = serialized.issuer; + process.uuid = serialized.uuid; + process.audience = serialized.audience; + return process; + } + + private constructor() { + /* see .start() */ + } + + static async start(args: string): Promise { + const oidcProc = new this(); + debug('spawning OIDC child process', [process.execPath, __filename], args); + const proc = spawn(process.execPath, [__filename], { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + RUN_OIDC_MOCK_PROVIDER: args, + }, + detached: true, + serialization: 'advanced', + }); + await once(proc, 'spawn'); + try { + oidcProc.pid = proc.pid; + const [msg] = await Promise.race([ + once(proc, 'message'), + once(proc, 'exit').then(() => { + throw new Error( + `OIDC mock provider process exited before sending message (${String(proc.exitCode)}, ${String(proc.signalCode)})`, + ); + }), + ]); + debug('received message from OIDC child process', msg); + oidcProc.issuer = msg.issuer; + oidcProc.uuid = msg.uuid; + oidcProc.audience = msg.audience; + } catch (err) { + proc.kill(); + throw err; + } + proc.unref(); + proc.channel?.unref(); + debug('OIDC setup complete, uuid =', oidcProc.uuid); + return oidcProc; + } + + async close(): Promise { + try { + if (this.pid) process.kill(this.pid, 0); + } catch (e) { + if (typeof e === 'object' && e && 'code' in e && e.code === 'ESRCH') + return; // process already exited + } + + if (!this.issuer || !this.uuid) return; + await fetch(new URL(this.issuer, `/shutdown/${this.uuid}`)); + this.uuid = undefined; + } +} diff --git a/packages/mongodb-runner/src/runner-helpers.ts b/packages/mongodb-runner/src/runner-helpers.ts index 55c30913..367aebb3 100644 --- a/packages/mongodb-runner/src/runner-helpers.ts +++ b/packages/mongodb-runner/src/runner-helpers.ts @@ -4,6 +4,8 @@ import type { MongoClusterOptions } from './mongocluster'; import { MongoCluster } from './mongocluster'; import { parallelForEach } from './util'; import * as fs from 'fs/promises'; +import { spawn } from 'child_process'; +import { once } from 'events'; interface StoredInstance { id: string; @@ -90,3 +92,43 @@ export async function stop(argv: { await fs.rm(instance.filepath); }); } + +export async function exec( + argv: { + id?: string; + runnerDir: string; + } & MongoClusterOptions, + args: string[], +) { + let mongodArgs: string[]; + let execArgs: string[]; + + const doubleDashIndex = args.indexOf('--'); + if (doubleDashIndex !== -1) { + mongodArgs = args.slice(0, doubleDashIndex); + execArgs = args.slice(doubleDashIndex + 1); + } else { + mongodArgs = []; + execArgs = args; + } + const cluster = await MongoCluster.start({ + ...argv, + args: mongodArgs, + }); + try { + const [prog, ...progArgs] = execArgs; + const child = spawn(prog, progArgs, { + stdio: 'inherit', + env: { + ...process.env, + // both spellings since otherwise I'd end up misspelling these half of the time + MONGODB_URI: cluster.connectionString, + MONGODB_URL: cluster.connectionString, + MONGODB_HOSTPORT: cluster.hostport, + }, + }); + [process.exitCode] = await once(child, 'exit'); + } finally { + await cluster.close(); + } +} diff --git a/packages/oidc-mock-provider/README.md b/packages/oidc-mock-provider/README.md index a7c7bc72..5f75d210 100644 --- a/packages/oidc-mock-provider/README.md +++ b/packages/oidc-mock-provider/README.md @@ -1,3 +1,66 @@ # @mongodb-js/oidc-mock-provider -For testing use only! +This is a package for internal testing. It should not be considered +production-ready or used in deployed applications. + +This OIDC provider does not provide any form of genuine authentication +or authorization functionality. + +## CLI Options + +The OIDC Mock Provider can be configured using the following command-line options: + +| Option | Alias | Type | Description | Default | +| ---------------------- | ----- | ------- | ----------------------------------------------------------------------------- | -------------------- | +| `--port` | `-p` | string | Port to run the server on. Setting to `0` auto-assigns a random port. | `0` or `defaultPort` | +| `--host` | `-h` | string | Hostname for the server to listen upon. | `localhost` | +| `--bind_ip_all` | | boolean | Bind to all IPv4 and IPv6 addresses. | `false` | +| `--payload` | `-P` | string | JSON payload to be returned to the client. | | +| `--expiry` | `-e` | number | Expiry time for the token in seconds. | `3600` | +| `--id-payload` | `-i` | string | Custom JSON payload for ID tokens. | | +| `--skip-id-token` | | boolean | Skip issuing ID tokens. | | +| `--skip-refresh-token` | | boolean | Skip issuing refresh tokens. | | +| `--log-requests` | `-l` | boolean | Log all incoming HTTP requests to the console. | `true` | +| `--issuer-metadata` | `-I` | string | Additional issuer metadata as JSON to include in the OIDC discovery document. | `{}` | + +The default token payload description is: + +```js +{ + groups: ['testgroup'], + sub: 'testuser', + aud: 'resource-server-audience-value', +} +``` + +## Examples + +Start the OIDC mock identity provider server on port 28200: + +```sh +npx @mongodb-js/oidc-mock-provider -p 28200 +``` + +Start the server on a random port and bind to all interfaces: + +```sh +npx @mongodb-js/oidc-mock-provider --port 0 --bind_ip_all +``` + +Return a custom JSON payload and set token expiry to 10 minutes: + +```sh +npx @mongodb-js/oidc-mock-provider -P '{"sub":"user123"}' -e 600 +``` + +Log all HTTP requests and provide additional issuer metadata: + +```sh +npx @mongodb-js/oidc-mock-provider -l --issuer-metadata '{"custom":"value"}' +``` + +For more options, run: + +```sh +npx @mongodb-js/oidc-mock-provider --help +``` diff --git a/packages/oidc-mock-provider/bin/oidc-mock-provider.js b/packages/oidc-mock-provider/bin/oidc-mock-provider.js old mode 100755 new mode 100644 index 46f5c201..94d44f2b --- a/packages/oidc-mock-provider/bin/oidc-mock-provider.js +++ b/packages/oidc-mock-provider/bin/oidc-mock-provider.js @@ -1,62 +1,3 @@ #!/usr/bin/env node -/* eslint-disable no-console */ 'use strict'; - -const { OIDCMockProvider } = require('..'); - -const DEFAULT_PORT = 28200; -const DEFAULT_HOST = 'localhost'; -const DEFAULT_BIND_IP_ALL = false; - -const argv = require('yargs') - .option('port', { - alias: 'p', - type: 'string', - desc: 'Port to run the server on. Defaults to 0 (auto-assigns to a random port).', - }) - .option('host', { - alias: 'h', - type: 'string', - desc: 'Hostname for the server to listen upon. Defaults to localhost.', - }) - .option('bind_ip_all', { - type: 'boolean', - desc: 'Bind to all IPv4 and IPv6 addresses', - }) - .example( - '$0 -p 28200', - 'Start the OIDC mock identity provider server on port 28200', - ) - .help().argv; - -const DEFAULT_TOKEN_PAYLOAD = { - expires_in: 3600, - payload: { - // Define the user information stored inside the access tokens. - groups: ['testgroup'], - sub: 'testuser', - aud: 'resource-server-audience-value', - }, -}; - -(async function main() { - const oidcMockProviderConfig = { - getTokenPayload() { - return DEFAULT_TOKEN_PAYLOAD; - }, - port: argv.port ?? DEFAULT_PORT, - hostname: argv.host ?? DEFAULT_HOST, - bindIpAll: argv.bind_ip_all ?? DEFAULT_BIND_IP_ALL, - }; - const mockIdentityProvider = await OIDCMockProvider.create( - oidcMockProviderConfig, - ); - - console.log( - `Started OIDC mock identity provider server. Listening on ${oidcMockProviderConfig.hostname}:${oidcMockProviderConfig.port}.`, - ); - console.log( - 'OIDC mock identity provider server issuer:', - mockIdentityProvider.issuer, - ); -})(); +require('../dist/cli'); diff --git a/packages/oidc-mock-provider/src/cli-parser.ts b/packages/oidc-mock-provider/src/cli-parser.ts new file mode 100644 index 00000000..a2f34846 --- /dev/null +++ b/packages/oidc-mock-provider/src/cli-parser.ts @@ -0,0 +1,101 @@ +import yargs from 'yargs'; +import type { OIDCMockProviderConfig } from '.'; + +const DEFAULT_TOKEN_PAYLOAD = JSON.stringify({ + groups: ['testgroup'], + sub: 'testuser', + aud: 'resource-server-audience-value', +}); + +export function parseCLIArgs( + args?: undefined | string | string[], + defaultPort?: number, +): OIDCMockProviderConfig { + const yargsParser = yargs + .option('port', { + alias: 'p', + type: 'string', + desc: 'Port to run the server on. Setting to 0 auto-assigns to a random port.', + default: defaultPort ?? 0, + }) + .option('host', { + alias: 'h', + type: 'string', + desc: 'Hostname for the server to listen upon. Defaults to localhost.', + default: 'localhost', + }) + .option('bind_ip_all', { + type: 'boolean', + desc: 'Bind to all IPv4 and IPv6 addresses', + default: false, + }) + .option('payload', { + alias: 'P', + type: 'string', + desc: 'JSON payload to be returned to the client.', + default: DEFAULT_TOKEN_PAYLOAD, + }) + .option('expiry', { + alias: 'e', + type: 'number', + desc: 'Expiry time for the token in seconds.', + default: 3600, + }) + .option('id-payload', { + alias: 'i', + type: 'string', + desc: 'Custom JSON payload for ID tokens.', + }) + .option('skip-id-token', { + type: 'boolean', + desc: 'Skip issuing ID tokens.', + }) + .option('skip-refresh-token', { + type: 'boolean', + desc: 'Skip issuing ID tokens.', + }) + .option('log-requests', { + alias: 'l', + type: 'boolean', + desc: 'Log all incoming HTTP requests to the console.', + default: true, + }) + .option('issuer-metadata', { + alias: 'I', + type: 'string', + desc: 'Additional issuer metadata as JSON to include in the OIDC discovery document.', + default: '{}', + }) + .example( + '$0 -p 28200', + 'Start the OIDC mock identity provider server on port 28200', + ) + .help(); + const argv = args ? yargsParser.parseSync(args) : yargsParser.parseSync(); + + return { + getTokenPayload() { + return { + expires_in: argv.expiry, + payload: JSON.parse(argv.payload), + skipIdToken: argv.skipIdToken, + customIdTokenPayload: argv.idPayload + ? JSON.parse(argv.idPayload) + : undefined, + skipRefreshToken: argv.skipRefreshToken, + }; + }, + port: +argv.port, + hostname: argv.host, + bindIpAll: argv.bind_ip_all, + overrideRequestHandler: argv.logRequests + ? undefined + : (url, req) => { + // eslint-disable-next-line no-console + console.log(`${String(req.method)} ${url}`); + }, + additionalIssuerMetadata() { + return JSON.parse(argv.issuerMetadata); + }, + }; +} diff --git a/packages/oidc-mock-provider/src/cli.ts b/packages/oidc-mock-provider/src/cli.ts new file mode 100755 index 00000000..3b1343d8 --- /dev/null +++ b/packages/oidc-mock-provider/src/cli.ts @@ -0,0 +1,24 @@ +#!/usr/bin/env node +/* eslint-disable no-console */ +'use strict'; + +import { OIDCMockProvider } from '.'; +import { parseCLIArgs } from './cli-parser'; + +(async function main() { + const oidcMockProviderConfig = parseCLIArgs(undefined, 28200); + const mockIdentityProvider = await OIDCMockProvider.create( + oidcMockProviderConfig, + ); + + console.log( + `Started OIDC mock identity provider server. Listening on ${String(oidcMockProviderConfig.hostname)}:${String(oidcMockProviderConfig.port)}.`, + ); + console.log( + 'OIDC mock identity provider server issuer:', + mockIdentityProvider.issuer, + ); +})().catch((err) => { + console.error('Error starting OIDC mock identity provider server:', err); + process.exitCode = 1; +}); diff --git a/packages/oidc-mock-provider/src/index.ts b/packages/oidc-mock-provider/src/index.ts index 814a0a00..2e936fd6 100644 --- a/packages/oidc-mock-provider/src/index.ts +++ b/packages/oidc-mock-provider/src/index.ts @@ -12,6 +12,8 @@ import type { KeyPairKeyObjectResult } from 'crypto'; import { promisify } from 'util'; import { URLSearchParams } from 'url'; +export { parseCLIArgs } from './cli-parser'; + async function randomString(n: number, enc: BufferEncoding) { return (await promisify(crypto.randomBytes)(n)).toString(enc); } @@ -43,6 +45,7 @@ export interface OIDCMockProviderConfig { payload: Record; customIdTokenPayload?: Record; skipIdToken?: boolean; + skipRefreshToken?: boolean; }>; /** @@ -270,20 +273,23 @@ export class OIDCMockProvider { } } - const { access_token, id_token, expires_in } = await this.issueToken({ - client_id, - scope, - nonce, - }); + const { access_token, id_token, expires_in, skipRefreshToken } = + await this.issueToken({ + client_id, + scope, + nonce, + }); // Issue a token response: result = { access_token: access_token, id_token: id_token, - refresh_token: await this.storeForSingleRetrieval({ - id_token, - access_token, - }), + refresh_token: skipRefreshToken + ? undefined + : await this.storeForSingleRetrieval({ + id_token, + access_token, + }), token_type: 'Bearer', expires_in, }; @@ -334,9 +340,15 @@ export class OIDCMockProvider { expires_in: number; access_token: string; id_token: string | undefined; + skipRefreshToken: boolean; }> { - const { expires_in, payload, skipIdToken, customIdTokenPayload } = - await this.config.getTokenPayload(metadata); + const { + expires_in, + payload, + skipIdToken, + skipRefreshToken, + customIdTokenPayload, + } = await this.config.getTokenPayload(metadata); const currentTimeInSeconds = Math.floor(Date.now() / 1000); const header = { alg: 'RS256', @@ -376,6 +388,7 @@ export class OIDCMockProvider { aud: metadata.client_id, ...customIdTokenPayload, }), + skipRefreshToken: !!skipRefreshToken, }; } From eaa51a855154bbe56c0e9a7ad55c5aeee9e219a1 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 5 Nov 2025 20:02:02 -0500 Subject: [PATCH 2/5] chmod +x bin/oidc-mock-provider.js --- packages/oidc-mock-provider/bin/oidc-mock-provider.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100644 => 100755 packages/oidc-mock-provider/bin/oidc-mock-provider.js diff --git a/packages/oidc-mock-provider/bin/oidc-mock-provider.js b/packages/oidc-mock-provider/bin/oidc-mock-provider.js old mode 100644 new mode 100755 From e6409857e41d0111733b0b8023460c9c7993db7b Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Wed, 5 Nov 2025 20:56:29 -0500 Subject: [PATCH 3/5] fixup: requirements in READMEs --- packages/mongodb-runner/README.md | 5 +++++ packages/oidc-mock-provider/README.md | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/packages/mongodb-runner/README.md b/packages/mongodb-runner/README.md index 48ab3d37..17b8c663 100644 --- a/packages/mongodb-runner/README.md +++ b/packages/mongodb-runner/README.md @@ -2,6 +2,11 @@ Helper for spinning up MongoDB servers and clusters for testing. +## Requirements + +Node.js >= 20.19.5, npm >= 11.6.0. Running as `npx mongodb-runner ...` +is typically the easiest way to install/run this tool. + ## Example usage > Note: Version 5 of mongodb-runner is a full re-write. Many things work diff --git a/packages/oidc-mock-provider/README.md b/packages/oidc-mock-provider/README.md index 5f75d210..7fe8a344 100644 --- a/packages/oidc-mock-provider/README.md +++ b/packages/oidc-mock-provider/README.md @@ -6,6 +6,12 @@ production-ready or used in deployed applications. This OIDC provider does not provide any form of genuine authentication or authorization functionality. +## Requirements + +Node.js >= 20.19.5, npm >= 11.6.0. Running as +`npx @mongodb-js/oidc-mock-provider ...` is typically the easiest way +to install/run this tool. + ## CLI Options The OIDC Mock Provider can be configured using the following command-line options: From 9fb4455b1b67291c34c82e645cf713a1584e3b2c Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 6 Nov 2025 13:53:12 -0500 Subject: [PATCH 4/5] fixup: log OIDC connection string as well when using --oidc --- packages/mongodb-runner/src/cli.ts | 10 +++++++++- packages/mongodb-runner/src/mongocluster.ts | 10 +++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index 861a4acd..7a8a75be 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -4,6 +4,8 @@ import os from 'os'; import path from 'path'; import createDebug from 'debug'; import * as utilities from './index'; +import { ConnectionString } from 'mongodb-connection-string-url'; +import { MongoClientOptions } from 'mongodb'; (async function () { const defaultRunnerDir = path.join(os.homedir(), '.mongodb', 'runner2'); @@ -101,9 +103,15 @@ import * as utilities from './index'; async function start() { const { cluster, id } = await utilities.start(argv, args); - console.log(`Server started and running at ${cluster.connectionString}`); + const cs = new ConnectionString(cluster.connectionString); + console.log(`Server started and running at ${cs.toString()}`); if (cluster.oidcIssuer) { + cs.typedSearchParams().set( + 'authMechanism', + 'MONGODB-OIDC', + ); console.log(`OIDC provider started and running at ${cluster.oidcIssuer}`); + console.log(`Server connection string with OIDC auth: ${cs.toString()}`); } console.log('Run the following command to stop the instance:'); console.log( diff --git a/packages/mongodb-runner/src/mongocluster.ts b/packages/mongodb-runner/src/mongocluster.ts index 8437b70d..6fc67891 100644 --- a/packages/mongodb-runner/src/mongocluster.ts +++ b/packages/mongodb-runner/src/mongocluster.ts @@ -82,9 +82,13 @@ export class MongoCluster { } get connectionString(): string { - return `mongodb://${this.hostport}/${ - this.replSetName ? `?replicaSet=${this.replSetName}` : '' - }`; + const cs = new ConnectionString(`mongodb://${this.hostport}/`); + if (this.replSetName) + cs.typedSearchParams().set( + 'replicaSet', + this.replSetName, + ); + return cs.toString(); } get oidcIssuer(): string | undefined { From e699e95cb0eef3d3a8aa3407b69f47f0aed33da7 Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 6 Nov 2025 13:54:34 -0500 Subject: [PATCH 5/5] fixup! fixup: log OIDC connection string as well when using --oidc --- packages/mongodb-runner/src/cli.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/mongodb-runner/src/cli.ts b/packages/mongodb-runner/src/cli.ts index 7a8a75be..a63472dd 100644 --- a/packages/mongodb-runner/src/cli.ts +++ b/packages/mongodb-runner/src/cli.ts @@ -5,7 +5,7 @@ import path from 'path'; import createDebug from 'debug'; import * as utilities from './index'; import { ConnectionString } from 'mongodb-connection-string-url'; -import { MongoClientOptions } from 'mongodb'; +import type { MongoClientOptions } from 'mongodb'; (async function () { const defaultRunnerDir = path.join(os.homedir(), '.mongodb', 'runner2');