Skip to content

Commit a581305

Browse files
authored
fix(devtools-connect): allow AWS auth using connection string with driver 7.x COMPASS-9911 (#612)
The Node.js driver removed support for specifying AWS auth options in the connection string in 7.x. Since we do not (currently) want to push this breaking change onto our users for the time being, we add a translation layer that accounts for this change.
1 parent 81c2cb8 commit a581305

File tree

3 files changed

+162
-0
lines changed

3 files changed

+162
-0
lines changed
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { expect } from 'chai';
2+
import { transformAWSAuthMechanismOptions } from './aws-auth-compat';
3+
import { MongoClientOptions } from 'mongodb';
4+
5+
describe('transformAWSAuthMechanismOptions', function () {
6+
it('returns original uri and options if authMechanism is not MONGODB-AWS', function () {
7+
const uri = 'mongodb://user:pass@host:27017/db?authMechanism=SCRAM-SHA-1';
8+
const clientOptions: MongoClientOptions = {};
9+
const result = transformAWSAuthMechanismOptions({ uri, clientOptions });
10+
expect(result.uri).to.equal(uri);
11+
expect(result.clientOptions).to.equal(clientOptions);
12+
});
13+
14+
it('transforms uri and options when MONGODB-AWS auth with credentials in connection string', async function () {
15+
const uri =
16+
'mongodb://accessKey:secretKey@host:27017/db?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:sessionToken';
17+
const clientOptions: MongoClientOptions = {};
18+
const result = transformAWSAuthMechanismOptions({ uri, clientOptions });
19+
expect(result.uri).to.equal(
20+
'mongodb://host:27017/db?authMechanism=MONGODB-AWS',
21+
);
22+
expect(result.clientOptions.auth).to.equal(undefined);
23+
expect(result.clientOptions.authMechanismProperties).to.have.property(
24+
'AWS_CREDENTIAL_PROVIDER',
25+
);
26+
const creds =
27+
await result.clientOptions.authMechanismProperties?.AWS_CREDENTIAL_PROVIDER?.();
28+
expect(creds).to.deep.equal({
29+
accessKeyId: 'accessKey',
30+
secretAccessKey: 'secretKey',
31+
sessionToken: 'sessionToken',
32+
});
33+
});
34+
35+
it('handles special characters', async function () {
36+
const uri = `mongodb://${encodeURIComponent('usernäme')}:${encodeURIComponent('passwõrd')}@host:27017/db?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:${encodeURIComponent('a+b=c/d')}`;
37+
const clientOptions: MongoClientOptions = {};
38+
const result = transformAWSAuthMechanismOptions({ uri, clientOptions });
39+
expect(result.uri).to.equal(
40+
'mongodb://host:27017/db?authMechanism=MONGODB-AWS',
41+
);
42+
expect(result.clientOptions.auth).to.equal(undefined);
43+
expect(result.clientOptions.authMechanismProperties).to.have.property(
44+
'AWS_CREDENTIAL_PROVIDER',
45+
);
46+
const creds =
47+
await result.clientOptions.authMechanismProperties?.AWS_CREDENTIAL_PROVIDER?.();
48+
expect(creds).to.deep.equal({
49+
accessKeyId: 'usernäme',
50+
secretAccessKey: 'passwõrd',
51+
sessionToken: 'a+b=c/d',
52+
});
53+
});
54+
55+
it('leaves unrelated auth mechanism properties intact', async function () {
56+
const uri =
57+
'mongodb://accessKey:secretKey@host:27017/db?authMechanism=MONGODB-AWS&authMechanismProperties=AWS_SESSION_TOKEN:sessionToken,FOO:BAR';
58+
const clientOptions: MongoClientOptions = {};
59+
const result = transformAWSAuthMechanismOptions({ uri, clientOptions });
60+
expect(result.uri).to.equal(
61+
'mongodb://host:27017/db?authMechanism=MONGODB-AWS&authMechanismProperties=FOO%3ABAR',
62+
);
63+
expect(result.clientOptions.auth).to.equal(undefined);
64+
expect(result.clientOptions.authMechanismProperties).to.have.property(
65+
'AWS_CREDENTIAL_PROVIDER',
66+
);
67+
const creds =
68+
await result.clientOptions.authMechanismProperties?.AWS_CREDENTIAL_PROVIDER?.();
69+
expect(creds).to.deep.equal({
70+
accessKeyId: 'accessKey',
71+
secretAccessKey: 'secretKey',
72+
sessionToken: 'sessionToken',
73+
});
74+
});
75+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {
2+
CommaAndColonSeparatedRecord,
3+
ConnectionString,
4+
} from 'mongodb-connection-string-url';
5+
import type {
6+
AuthMechanismProperties,
7+
AWSCredentials,
8+
MongoClientOptions,
9+
} from 'mongodb';
10+
11+
// The 7.x driver supports AWS authentication via callback-based providers,
12+
// and removed support for credentials passed through the connection string
13+
// (NODE-7046).
14+
// We add the support back here via a custom callback-based provider when
15+
// we detect that the user is trying to use MONGODB-AWS auth with credentials
16+
// in the connection string.
17+
export function transformAWSAuthMechanismOptions<T extends MongoClientOptions>({
18+
uri,
19+
clientOptions,
20+
}: {
21+
uri: string;
22+
clientOptions: T;
23+
}): { uri: string; clientOptions: T } {
24+
let connectionString: ConnectionString;
25+
try {
26+
connectionString = new ConnectionString(uri);
27+
} catch {
28+
return { uri, clientOptions };
29+
}
30+
const searchParams = connectionString.typedSearchParams<MongoClientOptions>();
31+
if (
32+
(clientOptions.authMechanism ?? searchParams.get('authMechanism')) !==
33+
'MONGODB-AWS'
34+
) {
35+
return { uri, clientOptions };
36+
}
37+
const username =
38+
clientOptions.auth?.username ??
39+
decodeURIComponent(connectionString.username);
40+
const password =
41+
clientOptions.auth?.password ??
42+
decodeURIComponent(connectionString.password);
43+
const authMechProps = new CommaAndColonSeparatedRecord(
44+
searchParams.get('authMechanismProperties') ?? '',
45+
);
46+
const sessionToken =
47+
clientOptions.authMechanismProperties?.AWS_SESSION_TOKEN ??
48+
authMechProps.get('AWS_SESSION_TOKEN');
49+
if (username || password || sessionToken) {
50+
// First, remove all relevant options from the connection string
51+
connectionString.username = '';
52+
connectionString.password = '';
53+
authMechProps.delete('AWS_SESSION_TOKEN');
54+
if (authMechProps.size === 0) {
55+
searchParams.delete('authMechanismProperties');
56+
} else {
57+
searchParams.set('authMechanismProperties', authMechProps.toString());
58+
}
59+
// Set AWS_CREDENTIAL_PROVIDER in the programmatic options,
60+
// and unset all other credential options.
61+
const programmaticAuthMechProps: AuthMechanismProperties & {
62+
AWS_SESSION_TOKEN?: unknown;
63+
} = { ...clientOptions.authMechanismProperties };
64+
delete programmaticAuthMechProps.AWS_SESSION_TOKEN;
65+
programmaticAuthMechProps.AWS_CREDENTIAL_PROVIDER ??=
66+
(): Promise<AWSCredentials> => {
67+
const creds: AWSCredentials = {
68+
accessKeyId: username,
69+
secretAccessKey: password,
70+
sessionToken: sessionToken || undefined,
71+
};
72+
return Promise.resolve(creds);
73+
};
74+
clientOptions = {
75+
...clientOptions,
76+
auth: undefined,
77+
authMechanismProperties: programmaticAuthMechProps,
78+
};
79+
}
80+
return { uri: connectionString.toString(), clientOptions };
81+
}

packages/devtools-connect/src/connect.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import {
3434
createFetch,
3535
isExistingAgentInstance,
3636
} from '@mongodb-js/devtools-proxy-support';
37+
import { transformAWSAuthMechanismOptions } from './aws-auth-compat';
3738
export type { DevtoolsProxyOptions, AgentWithInitialize };
3839

3940
function isAtlas(str: string): boolean {
@@ -479,6 +480,11 @@ async function connectMongoClientImpl({
479480
MongoClientClass: typeof MongoClient;
480481
useSystemCA: boolean;
481482
}): Promise<ConnectMongoClientResult> {
483+
({ uri, clientOptions } = transformAWSAuthMechanismOptions({
484+
uri,
485+
clientOptions,
486+
}));
487+
482488
const cleanupOnClientClose: (() => void | Promise<void>)[] = [];
483489
const runClose = async () => {
484490
let item: (() => void | Promise<void>) | undefined;

0 commit comments

Comments
 (0)