feat(mongodb): support OIDC browser authentication
This commit is contained in:
22 files changed
+1109
-24
No files matched your search
+25
-1
@@ -1,5 +1,14 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { ACCESS_AGENT_MIN_CONNECT_TIMEOUT_SECS, AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS, connectionAttemptOriginalErrorMessage, connectionAttemptTimeoutMessage, connectionAttemptTimeoutMs, CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS } from "@/lib/connection/connectionAttemptTimeout";
|
||||
import {
|
||||
ACCESS_AGENT_MIN_CONNECT_TIMEOUT_SECS,
|
||||
AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS,
|
||||
connectionAttemptOriginalErrorMessage,
|
||||
connectionAttemptTimeoutMessage,
|
||||
connectionAttemptTimeoutMs,
|
||||
CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS,
|
||||
MONGO_LEGACY_FALLBACK_TIMEOUT_BUFFER_MS,
|
||||
MONGO_OIDC_BROWSER_AUTH_TIMEOUT_MS,
|
||||
} from "@/lib/connection/connectionAttemptTimeout";
|
||||
|
||||
describe("connectionAttemptTimeout", () => {
|
||||
it("uses a 10s default for regular database connections", () => {
|
||||
@@ -42,6 +51,21 @@ describe("connectionAttemptTimeout", () => {
|
||||
expect(connectionAttemptTimeoutMs({ db_type: "access", connect_timeout_secs: 45, transport_layers: [] })).toBe(47_000);
|
||||
});
|
||||
|
||||
it("keeps the legacy fallback buffer for regular MongoDB connections", () => {
|
||||
expect(connectionAttemptTimeoutMs({ db_type: "mongodb", connect_timeout_secs: 5, transport_layers: [] })).toBe(5_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS + MONGO_LEGACY_FALLBACK_TIMEOUT_BUFFER_MS);
|
||||
});
|
||||
|
||||
it("allows the MongoDB OIDC browser flow to use its full five-minute budget", () => {
|
||||
expect(
|
||||
connectionAttemptTimeoutMs({
|
||||
db_type: "mongodb",
|
||||
connect_timeout_secs: 5,
|
||||
transport_layers: [],
|
||||
connection_string: "mongodb+srv://cluster.example.com/app?authMechanism=MONGODB-OIDC&authSource=%24external",
|
||||
}),
|
||||
).toBe(5_000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS + MONGO_OIDC_BROWSER_AUTH_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
it("adds HTTP tunnel setup and tunneled endpoint probe budgets", () => {
|
||||
expect(
|
||||
connectionAttemptTimeoutMs({
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import type { ConnectionConfig, DatabaseType, TransportLayerConfig, TunnelProfile } from "@/types/database";
|
||||
import { mongoConnectionUsesOidc } from "@/lib/mongo/mongoConnectionOptions";
|
||||
|
||||
export const CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS = 2_000;
|
||||
export const MONGO_LEGACY_FALLBACK_TIMEOUT_BUFFER_MS = 30_000;
|
||||
export const MONGO_OIDC_BROWSER_AUTH_TIMEOUT_MS = 5 * 60_000;
|
||||
export const AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS = 30;
|
||||
export const ACCESS_AGENT_MIN_CONNECT_TIMEOUT_SECS = 30;
|
||||
const DEFAULT_CONNECT_TIMEOUT_SECS = 10;
|
||||
@@ -66,7 +68,7 @@ function resolvedTimeoutLayer(layer: TransportLayerConfig, resolveTunnelProfile?
|
||||
return { ...profile, id: layer.id, enabled: layer.enabled, profile_id: layer.profile_id } as TransportLayerConfig;
|
||||
}
|
||||
|
||||
export function connectionAttemptTimeoutMs(config: Pick<ConnectionConfig, "connect_timeout_secs" | "transport_layers"> & Partial<Pick<ConnectionConfig, "db_type">>, resolveTunnelProfile?: TunnelProfileResolver): number {
|
||||
export function connectionAttemptTimeoutMs(config: Pick<ConnectionConfig, "connect_timeout_secs" | "transport_layers"> & Partial<Pick<ConnectionConfig, "db_type" | "url_params" | "connection_string">>, resolveTunnelProfile?: TunnelProfileResolver): number {
|
||||
const baseTimeoutSecs = positiveSeconds(config.connect_timeout_secs, DEFAULT_CONNECT_TIMEOUT_SECS);
|
||||
const agentMinTimeoutSecs = config.db_type === "access" ? ACCESS_AGENT_MIN_CONNECT_TIMEOUT_SECS : AGENT_DRIVER_MIN_CONNECT_TIMEOUT_SECS;
|
||||
let timeoutSecs = DRIVER_STARTUP_FLOOR_TYPES.has(config.db_type as DatabaseType) ? Math.max(baseTimeoutSecs, agentMinTimeoutSecs) : baseTimeoutSecs;
|
||||
@@ -83,8 +85,10 @@ export function connectionAttemptTimeoutMs(config: Pick<ConnectionConfig, "conne
|
||||
// happen sequentially in the backend. Keep the UI guard outside their total
|
||||
// budget so it cannot cancel a connection attempt that is still progressing.
|
||||
if (hasEnabledTransportLayer) timeoutSecs += baseTimeoutSecs;
|
||||
const fallbackBuffer = config.db_type === "mongodb" ? MONGO_LEGACY_FALLBACK_TIMEOUT_BUFFER_MS : 0;
|
||||
return Math.ceil(timeoutSecs * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS + fallbackBuffer);
|
||||
const usesMongoOidc = config.db_type === "mongodb" && mongoConnectionUsesOidc(config.url_params, config.connection_string);
|
||||
const fallbackBuffer = config.db_type === "mongodb" && !usesMongoOidc ? MONGO_LEGACY_FALLBACK_TIMEOUT_BUFFER_MS : 0;
|
||||
const browserAuthBuffer = usesMongoOidc ? MONGO_OIDC_BROWSER_AUTH_TIMEOUT_MS : 0;
|
||||
return Math.ceil(timeoutSecs * 1000 + CONNECTION_ATTEMPT_TIMEOUT_BUFFER_MS + fallbackBuffer + browserAuthBuffer);
|
||||
}
|
||||
|
||||
export function connectionAttemptTimeoutMessage(timeoutMs: number): string {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { mongoConnectionUsesOidc } from "@/lib/mongo/mongoConnectionOptions";
|
||||
|
||||
describe("mongoConnectionUsesOidc", () => {
|
||||
it("detects OIDC in structured URL params without case sensitivity", () => {
|
||||
expect(mongoConnectionUsesOidc("authSource=%24external&AUTHMECHANISM=mongodb-oidc")).toBe(true);
|
||||
});
|
||||
|
||||
it("detects OIDC in mongodb and mongodb+srv connection strings", () => {
|
||||
expect(mongoConnectionUsesOidc(undefined, "mongodb://localhost/app?authMechanism=MONGODB-OIDC")).toBe(true);
|
||||
expect(mongoConnectionUsesOidc(undefined, "mongodb+srv://cluster.example.com/app?authMechanism=MONGODB%2DOIDC#fragment")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not classify non-OIDC or non-MongoDB URLs as OIDC", () => {
|
||||
expect(mongoConnectionUsesOidc("authMechanism=SCRAM-SHA-256")).toBe(false);
|
||||
expect(mongoConnectionUsesOidc(undefined, "postgres://localhost/app?authMechanism=MONGODB-OIDC")).toBe(false);
|
||||
});
|
||||
|
||||
it("treats a connection string as authoritative over stale form params", () => {
|
||||
expect(mongoConnectionUsesOidc("authMechanism=MONGODB-OIDC", "mongodb://localhost/app?authMechanism=SCRAM-SHA-256")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,20 @@ export function mongoUrlParam(urlParams: string | undefined, key: string): strin
|
||||
return params.get(key) || "";
|
||||
}
|
||||
|
||||
export function mongoConnectionUsesOidc(urlParams: string | undefined, connectionString?: string): boolean {
|
||||
const input = connectionString?.trim() || "";
|
||||
if (input) {
|
||||
if (!/^mongodb(?:\+srv)?:\/\//i.test(input)) return false;
|
||||
const queryStart = input.indexOf("?");
|
||||
if (queryStart < 0) return false;
|
||||
const fragmentStart = input.indexOf("#", queryStart + 1);
|
||||
const query = input.slice(queryStart + 1, fragmentStart < 0 ? undefined : fragmentStart);
|
||||
return mongoUrlParamCaseInsensitive(query, "authMechanism").toUpperCase() === "MONGODB-OIDC";
|
||||
}
|
||||
|
||||
return mongoUrlParamCaseInsensitive(urlParams, "authMechanism").toUpperCase() === "MONGODB-OIDC";
|
||||
}
|
||||
|
||||
export function setMongoUrlParam(urlParams: string | undefined, key: string, value: string): string {
|
||||
const params = parseMongoUrlParams(urlParams);
|
||||
const normalized = value.trim();
|
||||
@@ -84,3 +98,11 @@ export function mongodbAuthFailureHint(message: string): string {
|
||||
function parseMongoUrlParams(urlParams: string | undefined): URLSearchParams {
|
||||
return new URLSearchParams((urlParams || "").trim().replace(/^\?/, ""));
|
||||
}
|
||||
|
||||
function mongoUrlParamCaseInsensitive(urlParams: string | undefined, key: string): string {
|
||||
const expectedKey = key.toLowerCase();
|
||||
for (const [paramKey, value] of parseMongoUrlParams(urlParams)) {
|
||||
if (paramKey.toLowerCase() === expectedKey) return value;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
Reference in new issue
Block a user