Skip to content

Commit c7856f0

Browse files
committed
chore: implementing OIDC callbacks and flow detection
1 parent b01f267 commit c7856f0

File tree

3 files changed

+162
-18
lines changed

3 files changed

+162
-18
lines changed

src/common/connectionManager.ts

Lines changed: 121 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,23 @@
1-
import { driverOptions } from "./config.js";
1+
import { config, UserConfig, driverOptions } from "./config.js";
22
import { NodeDriverServiceProvider } from "@mongosh/service-provider-node-driver";
33
import EventEmitter from "events";
44
import { setAppNameParamIfMissing } from "../helpers/connectionOptions.js";
55
import { packageInfo } from "./packageInfo.js";
66
import ConnectionString from "mongodb-connection-string-url";
7-
import { MongoClientOptions } from "mongodb";
7+
import { MongoClientOptions, OIDCCallbackParams } from "mongodb";
88
import { ErrorCodes, MongoDBError } from "./errors.js";
9+
import type { MongoshBus } from "@mongosh/types";
10+
import { CompositeLogger, LogId } from "./logger.js";
11+
12+
// https://github.com/mongodb-js/oidc-plugin/blob/main/src/types.ts
13+
const MONGODB_OIDC_CLIENT_ERROR_EVENTS = [
14+
"mongodb-oidc-plugin:deserialization-failed",
15+
"mongodb-oidc-plugin:local-listen-failed",
16+
"mongodb-oidc-plugin:auth-attempt-failed",
17+
"mongodb-oidc-plugin:refresh-failed",
18+
"mongodb-oidc-plugin:auth-failed",
19+
"mongodb-oidc-plugin:outbound-http-request-failed",
20+
] as const;
921

1022
export interface AtlasClusterConnectionInfo {
1123
username: string;
@@ -67,11 +79,25 @@ export interface ConnectionManagerEvents {
6779

6880
export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
6981
private state: AnyConnectionState;
82+
private bus: MongoshBus;
7083

71-
constructor() {
84+
constructor(
85+
private logger: CompositeLogger,
86+
bus?: MongoshBus
87+
) {
7288
super();
7389

90+
this.bus = bus ?? new EventEmitter();
7491
this.state = { tag: "disconnected" };
92+
93+
for (const clientErrorEvent of MONGODB_OIDC_CLIENT_ERROR_EVENTS) {
94+
this.bus.on(clientErrorEvent, this.onOidcClientErrorCallback.bind(this, clientErrorEvent));
95+
}
96+
97+
this.bus.on("mongodb-oidc-plugin:received-server-params", this.onOidcReceivedServerParameters.bind(this));
98+
this.bus.on("mongodb-oidc-plugin:auth-failed", this.onOidcAuthenticationFailed.bind(this));
99+
this.bus.on("mongodb-oidc-plugin:auth-succeeded", this.onOidcAuthSucceeded.bind(this));
100+
this.bus.on("mongodb-oidc-plugin:notify-device-flow", this.onOidcNotifyDeviceFlow.bind(this));
75101
}
76102

77103
async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
@@ -89,11 +115,16 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
89115
defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
90116
});
91117

92-
serviceProvider = await NodeDriverServiceProvider.connect(settings.connectionString, {
93-
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
94-
productName: "MongoDB MCP",
95-
...driverOptions,
96-
});
118+
serviceProvider = await NodeDriverServiceProvider.connect(
119+
settings.connectionString,
120+
{
121+
productDocsLink: "https://github.com/mongodb-js/mongodb-mcp-server/",
122+
productName: "MongoDB MCP",
123+
...driverOptions,
124+
},
125+
undefined,
126+
this.bus
127+
);
97128
} catch (error: unknown) {
98129
const errorReason = error instanceof Error ? error.message : `${error as string}`;
99130
this.changeState("connection-errored", {
@@ -111,7 +142,7 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
111142
tag: "connected",
112143
connectedAtlasCluster: settings.atlas,
113144
serviceProvider,
114-
connectionStringAuthType: ConnectionManager.inferConnectionTypeFromSettings(settings),
145+
connectionStringAuthType: ConnectionManager.inferConnectionTypeFromSettings(config, settings),
115146
});
116147
} catch (error: unknown) {
117148
const errorReason = error instanceof Error ? error.message : `${error as string}`;
@@ -157,13 +188,92 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
157188
return newState;
158189
}
159190

160-
static inferConnectionTypeFromSettings(settings: ConnectionSettings): ConnectionStringAuthType {
191+
/**
192+
* This handles OIDC errors happening at the client side. There is a difference between errors happening
193+
* here and rejections from the server, so we will treat them differently.
194+
*/
195+
private onOidcClientErrorCallback(event: string, eventData: { error: string }): void {
196+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
197+
this.changeState("connection-errored", { tag: "errored", errorReason: eventData.error });
198+
}
199+
200+
// If we are not in the connecting state, we really can't do much with this. It might be a
201+
// transitional error (connectivity issues), a refresh token issue (server side configuration),
202+
// anything. Log the error to get some context later for debugging.
203+
this.logger.error({
204+
id: LogId.oidcClientError,
205+
context: event,
206+
message: `Error during OIDC flow, at event ${event}: ${eventData.error}`,
207+
});
208+
}
209+
210+
private onOidcReceivedServerParameters(eventData: OIDCCallbackParams): void {
211+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
212+
this.changeState("connection-succeeded", { ...this.state, tag: "connected" });
213+
}
214+
215+
this.logger.info({
216+
id: LogId.oidcFlow,
217+
context: "mongodb-oidc-plugin:received-server-parameters",
218+
message: `Received server parameters successfully: ${JSON.stringify(eventData)}`,
219+
});
220+
}
221+
222+
private onOidcAuthenticationFailed(eventData: { error: string }): void {
223+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
224+
this.changeState("connection-succeeded", { ...this.state, tag: "connected" });
225+
}
226+
227+
this.logger.error({
228+
id: LogId.oidcAuthFailed,
229+
context: "mongodb-oidc-plugin:authentication-failed",
230+
message: `Could not authenticate: ${eventData.error}`,
231+
});
232+
}
233+
234+
private onOidcAuthSucceeded(): void {
235+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
236+
this.changeState("connection-succeeded", { ...this.state, tag: "connected" });
237+
}
238+
239+
this.logger.info({
240+
id: LogId.oidcFlow,
241+
context: "mongodb-oidc-plugin:auth-succeeded",
242+
message: "Authenticated successfully.",
243+
});
244+
}
245+
246+
private onOidcNotifyDeviceFlow(): void {
247+
if (this.state.tag === "connecting" && this.state.connectionStringAuthType?.startsWith("oidc")) {
248+
this.changeState("connection-requested", {
249+
...this.state,
250+
tag: "connecting",
251+
connectionStringAuthType: "oidc-device-flow",
252+
});
253+
}
254+
255+
this.logger.info({
256+
id: LogId.oidcFlow,
257+
context: "mongodb-oidc-plugin:notify-device-flow",
258+
message: "OIDC Flow changed automatically to device flow.",
259+
});
260+
}
261+
262+
static inferConnectionTypeFromSettings(config: UserConfig, settings: ConnectionSettings): ConnectionStringAuthType {
161263
const connString = new ConnectionString(settings.connectionString);
162264
const searchParams = connString.typedSearchParams<MongoClientOptions>();
163265

164266
switch (searchParams.get("authMechanism")) {
165267
case "MONGODB-OIDC": {
166-
return "oidc-auth-flow"; // TODO: depending on if we don't have a --browser later it can be oidc-device-flow
268+
if (config.transport === "stdio" && config.browser) {
269+
return "oidc-auth-flow";
270+
}
271+
272+
if (config.transport === "http" && config.httpHost === "127.0.0.1" && config.browser) {
273+
return "oidc-auth-flow";
274+
}
275+
276+
return "oidc-device-flow";
167277
}
168278
case "MONGODB-X509":
169279
return "x.509";

src/common/logger.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ export const LogId = {
5858
exportedDataListError: mongoLogId(1_007_006),
5959
exportedDataAutoCompleteError: mongoLogId(1_007_007),
6060
exportLockError: mongoLogId(1_007_008),
61+
62+
oidcFlow: mongoLogId(1_008_001),
63+
oidcClientError: mongoLogId(1_008_002),
64+
oidcAuthFailed: mongoLogId(1_008_003),
6165
} as const;
6266

6367
interface LogPayload {

tests/integration/common/connectionManager.test.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
ConnectionStateConnected,
55
ConnectionStringAuthType,
66
} from "../../../src/common/connectionManager.js";
7+
import type { UserConfig } from "../../../src/common/config.js";
78
import { describeWithMongoDB } from "../tools/mongodb/mongodbHelpers.js";
89
import { describe, beforeEach, expect, it, vi, afterEach } from "vitest";
910

@@ -136,23 +137,52 @@ describeWithMongoDB("Connection Manager", (integration) => {
136137

137138
describe("Connection Manager connection type inference", () => {
138139
const testCases = [
139-
{ connectionString: "mongodb://localhost:27017", connectionType: "scram" },
140-
{ connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-X509", connectionType: "x.509" },
141-
{ connectionString: "mongodb://localhost:27017?authMechanism=GSSAPI", connectionType: "kerberos" },
140+
{ userConfig: {}, connectionString: "mongodb://localhost:27017", connectionType: "scram" },
142141
{
142+
userConfig: {},
143+
connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-X509",
144+
connectionType: "x.509",
145+
},
146+
{
147+
userConfig: {},
148+
connectionString: "mongodb://localhost:27017?authMechanism=GSSAPI",
149+
connectionType: "kerberos",
150+
},
151+
{
152+
userConfig: {},
143153
connectionString: "mongodb://localhost:27017?authMechanism=PLAIN&authSource=$external",
144154
connectionType: "ldap",
145155
},
146-
{ connectionString: "mongodb://localhost:27017?authMechanism=PLAIN", connectionType: "scram" },
147-
{ connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC", connectionType: "oidc-auth-flow" },
156+
{ userConfig: {}, connectionString: "mongodb://localhost:27017?authMechanism=PLAIN", connectionType: "scram" },
157+
{
158+
userConfig: { transport: "stdio", browser: "firefox" },
159+
connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC",
160+
connectionType: "oidc-auth-flow",
161+
},
162+
{
163+
userConfig: { transport: "http", httpHost: "127.0.0.1", browser: "ie6" },
164+
connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC",
165+
connectionType: "oidc-auth-flow",
166+
},
167+
{
168+
userConfig: { transport: "http", httpHost: "0.0.0.0", browser: "ie6" },
169+
connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC",
170+
connectionType: "oidc-device-flow",
171+
},
172+
{
173+
userConfig: { transport: "stdio" },
174+
connectionString: "mongodb://localhost:27017?authMechanism=MONGODB-OIDC",
175+
connectionType: "oidc-device-flow",
176+
},
148177
] as {
178+
userConfig: Partial<UserConfig>;
149179
connectionString: string;
150180
connectionType: ConnectionStringAuthType;
151181
}[];
152182

153-
for (const { connectionString, connectionType } of testCases) {
183+
for (const { userConfig, connectionString, connectionType } of testCases) {
154184
it(`infers ${connectionType} from ${connectionString}`, () => {
155-
const actualConnectionType = ConnectionManager.inferConnectionTypeFromSettings({
185+
const actualConnectionType = ConnectionManager.inferConnectionTypeFromSettings(userConfig as UserConfig, {
156186
connectionString,
157187
});
158188

0 commit comments

Comments
 (0)