Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(NODE-3487): check for nullish aws mechanism property #2951

Merged
merged 3 commits into from
Aug 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
17 changes: 14 additions & 3 deletions src/cmap/auth/mongo_credentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,22 @@ 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;
password: string;
source: string;
db?: string;
mechanism?: AuthMechanism;
mechanismProperties: Document;
mechanismProperties: AuthMechanismProperties;
}

/**
Expand All @@ -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;
Expand All @@ -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
Expand Down
63 changes: 32 additions & 31 deletions src/cmap/auth/mongodb_aws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down Expand Up @@ -123,18 +136,15 @@ export class MongoDBAWS extends AuthProvider {
path: '/',
body
},
{
accessKeyId: username,
secretAccessKey: password,
token
Copy link
Contributor Author

@nbbeeken nbbeeken Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should've been sessionToken, I moved the whole object declaration above.

}
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 = {
Expand All @@ -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<MongoCredentials>) {
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')
Expand All @@ -183,6 +195,7 @@ function makeTempCredentials(credentials: MongoCredentials, callback: Callback<M
if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
request(
`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`,
undefined,
(err, res) => {
if (err) return callback(err);
done(res);
Expand Down Expand Up @@ -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 => {
Expand Down
51 changes: 45 additions & 6 deletions src/deps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};
};
}
Copy link
Contributor Author

@nbbeeken nbbeeken Aug 25, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I inlined our own definitions of the types to hold us strictly to our usage, also this catches the mistake of passing in token instead of sessionToken


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
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 2 additions & 7 deletions src/mongo_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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. */
Expand Down
9 changes: 9 additions & 0 deletions test/functional/mongodb_aws.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
});
});