diff --git a/package.json b/package.json index 5c075324ff..4fd9b73ce5 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,6 @@ "@istanbuljs/nyc-config-typescript": "^1.0.1", "@microsoft/api-extractor": "^7.18.6", "@microsoft/tsdoc-config": "^0.15.2", - "@types/aws4": "^1.5.1", "@types/chai": "^4.2.14", "@types/chai-subset": "^1.3.3", "@types/kerberos": "^1.1.0", diff --git a/src/cmap/auth/mongo_credentials.ts b/src/cmap/auth/mongo_credentials.ts index 1f0b2a326d..cbc4007ac8 100644 --- a/src/cmap/auth/mongo_credentials.ts +++ b/src/cmap/auth/mongo_credentials.ts @@ -25,6 +25,14 @@ function getDefaultAuthMechanism(ismaster?: Document): AuthMechanism { return AuthMechanism.MONGODB_CR; } +/** @public */ +export interface AuthMechanismProperties extends Document { + SERVICE_NAME?: string; + SERVICE_REALM?: string; + CANONICALIZE_HOST_NAME?: boolean; + AWS_SESSION_TOKEN?: string; +} + /** @public */ export interface MongoCredentialsOptions { username: string; @@ -32,7 +40,7 @@ export interface MongoCredentialsOptions { source: string; db?: string; mechanism?: AuthMechanism; - mechanismProperties: Document; + mechanismProperties: AuthMechanismProperties; } /** @@ -49,7 +57,7 @@ export class MongoCredentials { /** The method used to authenticate */ readonly mechanism: AuthMechanism; /** Special properties used by some types of auth mechanisms */ - readonly mechanismProperties: Document; + readonly mechanismProperties: AuthMechanismProperties; constructor(options: MongoCredentialsOptions) { this.username = options.username; @@ -70,7 +78,10 @@ export class MongoCredentials { this.password = process.env.AWS_SECRET_ACCESS_KEY; } - if (!this.mechanismProperties.AWS_SESSION_TOKEN && process.env.AWS_SESSION_TOKEN) { + if ( + this.mechanismProperties.AWS_SESSION_TOKEN == null && + process.env.AWS_SESSION_TOKEN != null + ) { this.mechanismProperties = { ...this.mechanismProperties, AWS_SESSION_TOKEN: process.env.AWS_SESSION_TOKEN diff --git a/src/cmap/auth/mongodb_aws.ts b/src/cmap/auth/mongodb_aws.ts index 1584028085..a6cb48ab9e 100644 --- a/src/cmap/auth/mongodb_aws.ts +++ b/src/cmap/auth/mongodb_aws.ts @@ -14,6 +14,7 @@ import type { BSONSerializeOptions } from '../../bson'; import { aws4 } from '../../deps'; import { AuthMechanism } from './defaultAuthProviders'; +import type { Binary } from 'bson'; const ASCII_N = 110; const AWS_RELATIVE_URI = 'http://169.254.170.2'; @@ -64,10 +65,19 @@ export class MongoDBAWS extends AuthProvider { return; } - const username = credentials.username; - const password = credentials.password; + const accessKeyId = credentials.username; + const secretAccessKey = credentials.password; + const sessionToken = credentials.mechanismProperties.AWS_SESSION_TOKEN; + + // If all three defined, include sessionToken, else include username and pass, else no credentials + const awsCredentials = + accessKeyId && secretAccessKey && sessionToken + ? { accessKeyId, secretAccessKey, sessionToken } + : accessKeyId && secretAccessKey + ? { accessKeyId, secretAccessKey } + : undefined; + const db = credentials.source; - const token = credentials.mechanismProperties.AWS_SESSION_TOKEN; crypto.randomBytes(32, (err, nonce) => { if (err) { callback(err); @@ -83,7 +93,10 @@ export class MongoDBAWS extends AuthProvider { connection.command(ns(`${db}.$cmd`), saslStart, undefined, (err, res) => { if (err) return callback(err); - const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions); + const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions) as { + s: Binary; + h: string; + }; const host = serverResponse.h; const serverNonce = serverResponse.s.buffer; if (serverNonce.length !== 64) { @@ -123,18 +136,15 @@ export class MongoDBAWS extends AuthProvider { path: '/', body }, - { - accessKeyId: username, - secretAccessKey: password, - token - } + awsCredentials ); - const authorization = options.headers.Authorization; - const date = options.headers['X-Amz-Date']; - const payload: AWSSaslContinuePayload = { a: authorization, d: date }; - if (token) { - payload.t = token; + const payload: AWSSaslContinuePayload = { + a: options.headers.Authorization, + d: options.headers['X-Amz-Date'] + }; + if (sessionToken) { + payload.t = sessionToken; } const saslContinue = { @@ -149,14 +159,16 @@ export class MongoDBAWS extends AuthProvider { } } -interface AWSCredentials { +interface AWSTempCredentials { AccessKeyId?: string; SecretAccessKey?: string; Token?: string; + RoleArn?: string; + Expiration?: Date; } function makeTempCredentials(credentials: MongoCredentials, callback: Callback) { - function done(creds: AWSCredentials) { + function done(creds: AWSTempCredentials) { if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) { callback( new MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials') @@ -183,6 +195,7 @@ function makeTempCredentials(credentials: MongoCredentials, callback: Callback { if (err) return callback(err); done(res); @@ -239,27 +252,15 @@ interface RequestOptions { headers?: http.OutgoingHttpHeaders; } -function request(uri: string, callback: Callback): void; -function request(uri: string, options: RequestOptions, callback: Callback): void; -function request(uri: string, _options: RequestOptions | Callback, _callback?: Callback) { - let options = _options as RequestOptions; - if ('function' === typeof _options) { - options = {}; - } - - let callback: Callback = _options as Callback; - if (_callback) { - callback = _callback; - } - - options = Object.assign( +function request(uri: string, _options: RequestOptions | undefined, callback: Callback) { + const options = Object.assign( { method: 'GET', timeout: 10000, json: true }, url.parse(uri), - options + _options ); const req = http.request(options, res => { diff --git a/src/deps.ts b/src/deps.ts index 7598625dbd..35bd4ebf44 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -109,12 +109,51 @@ try { saslprep = require('saslprep'); } catch {} // eslint-disable-line -export let aws4: typeof import('aws4') | { kModuleError: MongoMissingDependencyError } = - makeErrorModule( - new MongoMissingDependencyError( - 'Optional module `aws4` not found. Please install it to enable AWS authentication' - ) - ); +interface AWS4 { + /** + * Created these inline types to better assert future usage of this API + * @param options - options for request + * @param credentials - AWS credential details, sessionToken should be omitted entirely if its false-y + */ + sign( + options: { + path: '/'; + body: string; + host: string; + method: 'POST'; + headers: { + 'Content-Type': 'application/x-www-form-urlencoded'; + 'Content-Length': number; + 'X-MongoDB-Server-Nonce': string; + 'X-MongoDB-GS2-CB-Flag': 'n'; + }; + service: string; + region: string; + }, + credentials: + | { + accessKeyId: string; + secretAccessKey: string; + sessionToken: string; + } + | { + accessKeyId: string; + secretAccessKey: string; + } + | undefined + ): { + headers: { + Authorization: string; + 'X-Amz-Date': string; + }; + }; +} + +export let aws4: AWS4 | { kModuleError: MongoMissingDependencyError } = makeErrorModule( + new MongoMissingDependencyError( + 'Optional module `aws4` not found. Please install it to enable AWS authentication' + ) +); try { // Ensure you always wrap an optional require in the try block NODE-3199 diff --git a/src/index.ts b/src/index.ts index c725869105..ad62dff5a4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -160,7 +160,11 @@ export type { OperationTime, ResumeOptions } from './change_stream'; -export type { MongoCredentials, MongoCredentialsOptions } from './cmap/auth/mongo_credentials'; +export type { + MongoCredentials, + AuthMechanismProperties, + MongoCredentialsOptions +} from './cmap/auth/mongo_credentials'; export type { WriteProtocolMessageType, Query, diff --git a/src/mongo_client.ts b/src/mongo_client.ts index fa87a6635e..69aed2d361 100644 --- a/src/mongo_client.ts +++ b/src/mongo_client.ts @@ -27,7 +27,7 @@ import type { AuthMechanism } from './cmap/auth/defaultAuthProviders'; import type { Topology, TopologyEvents } from './sdam/topology'; import type { ClientSession, ClientSessionOptions } from './sessions'; import type { TagSet } from './sdam/server_description'; -import type { MongoCredentials } from './cmap/auth/mongo_credentials'; +import type { AuthMechanismProperties, MongoCredentials } from './cmap/auth/mongo_credentials'; import { parseOptions } from './connection_string'; import type { CompressorName } from './cmap/wire_protocol/compression'; import type { TLSSocketOptions, ConnectionOptions as TLSConnectionOptions } from 'tls'; @@ -157,12 +157,7 @@ export interface MongoClientOptions extends BSONSerializeOptions, SupportedNodeC /** Specify the authentication mechanism that MongoDB will use to authenticate the connection. */ authMechanism?: AuthMechanism; /** Specify properties for the specified authMechanism as a comma-separated list of colon-separated key-value pairs. */ - authMechanismProperties?: { - SERVICE_NAME?: string; - CANONICALIZE_HOST_NAME?: boolean; - SERVICE_REALM?: string; - [key: string]: any; - }; + authMechanismProperties?: AuthMechanismProperties; /** The size (in milliseconds) of the latency window for selecting among multiple suitable MongoDB instances. */ localThresholdMS?: number; /** Specifies how long (in milliseconds) to block for server selection before throwing an exception. */ diff --git a/test/functional/mongodb_aws.test.js b/test/functional/mongodb_aws.test.js index 9555d27007..ea8543ded5 100644 --- a/test/functional/mongodb_aws.test.js +++ b/test/functional/mongodb_aws.test.js @@ -40,4 +40,13 @@ describe('MONGODB-AWS', function () { }); }); }); + + it('should allow empty string in authMechanismProperties.AWS_SESSION_TOKEN to override AWS_SESSION_TOKEN environment variable', function () { + const client = this.configuration.newClient(this.configuration.url(), { + authMechanismProperties: { AWS_SESSION_TOKEN: '' } + }); + expect(client) + .to.have.nested.property('options.credentials.mechanismProperties.AWS_SESSION_TOKEN') + .that.equals(''); + }); });