mongodb_aws.js 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.MongoDBAWS = void 0;
  4. const crypto = require("crypto");
  5. const http = require("http");
  6. const url = require("url");
  7. const BSON = require("../../bson");
  8. const deps_1 = require("../../deps");
  9. const error_1 = require("../../error");
  10. const utils_1 = require("../../utils");
  11. const auth_provider_1 = require("./auth_provider");
  12. const mongo_credentials_1 = require("./mongo_credentials");
  13. const providers_1 = require("./providers");
  14. const ASCII_N = 110;
  15. const AWS_RELATIVE_URI = 'http://169.254.170.2';
  16. const AWS_EC2_URI = 'http://169.254.169.254';
  17. const AWS_EC2_PATH = '/latest/meta-data/iam/security-credentials';
  18. const bsonOptions = {
  19. promoteLongs: true,
  20. promoteValues: true,
  21. promoteBuffers: false,
  22. bsonRegExp: false
  23. };
  24. class MongoDBAWS extends auth_provider_1.AuthProvider {
  25. auth(authContext, callback) {
  26. const { connection, credentials } = authContext;
  27. if (!credentials) {
  28. return callback(new error_1.MongoMissingCredentialsError('AuthContext must provide credentials.'));
  29. }
  30. if ('kModuleError' in deps_1.aws4) {
  31. return callback(deps_1.aws4['kModuleError']);
  32. }
  33. const { sign } = deps_1.aws4;
  34. if ((0, utils_1.maxWireVersion)(connection) < 9) {
  35. callback(new error_1.MongoCompatibilityError('MONGODB-AWS authentication requires MongoDB version 4.4 or later'));
  36. return;
  37. }
  38. if (!credentials.username) {
  39. makeTempCredentials(credentials, (err, tempCredentials) => {
  40. if (err || !tempCredentials)
  41. return callback(err);
  42. authContext.credentials = tempCredentials;
  43. this.auth(authContext, callback);
  44. });
  45. return;
  46. }
  47. const accessKeyId = credentials.username;
  48. const secretAccessKey = credentials.password;
  49. const sessionToken = credentials.mechanismProperties.AWS_SESSION_TOKEN;
  50. // If all three defined, include sessionToken, else include username and pass, else no credentials
  51. const awsCredentials = accessKeyId && secretAccessKey && sessionToken
  52. ? { accessKeyId, secretAccessKey, sessionToken }
  53. : accessKeyId && secretAccessKey
  54. ? { accessKeyId, secretAccessKey }
  55. : undefined;
  56. const db = credentials.source;
  57. crypto.randomBytes(32, (err, nonce) => {
  58. if (err) {
  59. callback(err);
  60. return;
  61. }
  62. const saslStart = {
  63. saslStart: 1,
  64. mechanism: 'MONGODB-AWS',
  65. payload: BSON.serialize({ r: nonce, p: ASCII_N }, bsonOptions)
  66. };
  67. connection.command((0, utils_1.ns)(`${db}.$cmd`), saslStart, undefined, (err, res) => {
  68. if (err)
  69. return callback(err);
  70. const serverResponse = BSON.deserialize(res.payload.buffer, bsonOptions);
  71. const host = serverResponse.h;
  72. const serverNonce = serverResponse.s.buffer;
  73. if (serverNonce.length !== 64) {
  74. callback(
  75. // TODO(NODE-3483)
  76. new error_1.MongoRuntimeError(`Invalid server nonce length ${serverNonce.length}, expected 64`));
  77. return;
  78. }
  79. if (serverNonce.compare(nonce, 0, nonce.length, 0, nonce.length) !== 0) {
  80. // TODO(NODE-3483)
  81. callback(new error_1.MongoRuntimeError('Server nonce does not begin with client nonce'));
  82. return;
  83. }
  84. if (host.length < 1 || host.length > 255 || host.indexOf('..') !== -1) {
  85. // TODO(NODE-3483)
  86. callback(new error_1.MongoRuntimeError(`Server returned an invalid host: "${host}"`));
  87. return;
  88. }
  89. const body = 'Action=GetCallerIdentity&Version=2011-06-15';
  90. const options = sign({
  91. method: 'POST',
  92. host,
  93. region: deriveRegion(serverResponse.h),
  94. service: 'sts',
  95. headers: {
  96. 'Content-Type': 'application/x-www-form-urlencoded',
  97. 'Content-Length': body.length,
  98. 'X-MongoDB-Server-Nonce': serverNonce.toString('base64'),
  99. 'X-MongoDB-GS2-CB-Flag': 'n'
  100. },
  101. path: '/',
  102. body
  103. }, awsCredentials);
  104. const payload = {
  105. a: options.headers.Authorization,
  106. d: options.headers['X-Amz-Date']
  107. };
  108. if (sessionToken) {
  109. payload.t = sessionToken;
  110. }
  111. const saslContinue = {
  112. saslContinue: 1,
  113. conversationId: 1,
  114. payload: BSON.serialize(payload, bsonOptions)
  115. };
  116. connection.command((0, utils_1.ns)(`${db}.$cmd`), saslContinue, undefined, callback);
  117. });
  118. });
  119. }
  120. }
  121. exports.MongoDBAWS = MongoDBAWS;
  122. function makeTempCredentials(credentials, callback) {
  123. function done(creds) {
  124. if (!creds.AccessKeyId || !creds.SecretAccessKey || !creds.Token) {
  125. callback(new error_1.MongoMissingCredentialsError('Could not obtain temporary MONGODB-AWS credentials'));
  126. return;
  127. }
  128. callback(undefined, new mongo_credentials_1.MongoCredentials({
  129. username: creds.AccessKeyId,
  130. password: creds.SecretAccessKey,
  131. source: credentials.source,
  132. mechanism: providers_1.AuthMechanism.MONGODB_AWS,
  133. mechanismProperties: {
  134. AWS_SESSION_TOKEN: creds.Token
  135. }
  136. }));
  137. }
  138. // If the environment variable AWS_CONTAINER_CREDENTIALS_RELATIVE_URI
  139. // is set then drivers MUST assume that it was set by an AWS ECS agent
  140. if (process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI) {
  141. request(`${AWS_RELATIVE_URI}${process.env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`, undefined, (err, res) => {
  142. if (err)
  143. return callback(err);
  144. done(res);
  145. });
  146. return;
  147. }
  148. // Otherwise assume we are on an EC2 instance
  149. // get a token
  150. request(`${AWS_EC2_URI}/latest/api/token`, { method: 'PUT', json: false, headers: { 'X-aws-ec2-metadata-token-ttl-seconds': 30 } }, (err, token) => {
  151. if (err)
  152. return callback(err);
  153. // get role name
  154. request(`${AWS_EC2_URI}/${AWS_EC2_PATH}`, { json: false, headers: { 'X-aws-ec2-metadata-token': token } }, (err, roleName) => {
  155. if (err)
  156. return callback(err);
  157. // get temp credentials
  158. request(`${AWS_EC2_URI}/${AWS_EC2_PATH}/${roleName}`, { headers: { 'X-aws-ec2-metadata-token': token } }, (err, creds) => {
  159. if (err)
  160. return callback(err);
  161. done(creds);
  162. });
  163. });
  164. });
  165. }
  166. function deriveRegion(host) {
  167. const parts = host.split('.');
  168. if (parts.length === 1 || parts[1] === 'amazonaws') {
  169. return 'us-east-1';
  170. }
  171. return parts[1];
  172. }
  173. function request(uri, _options, callback) {
  174. const options = Object.assign({
  175. method: 'GET',
  176. timeout: 10000,
  177. json: true
  178. }, url.parse(uri), _options);
  179. const req = http.request(options, res => {
  180. res.setEncoding('utf8');
  181. let data = '';
  182. res.on('data', d => (data += d));
  183. res.on('end', () => {
  184. if (options.json === false) {
  185. callback(undefined, data);
  186. return;
  187. }
  188. try {
  189. const parsed = JSON.parse(data);
  190. callback(undefined, parsed);
  191. }
  192. catch (err) {
  193. // TODO(NODE-3483)
  194. callback(new error_1.MongoRuntimeError(`Invalid JSON response: "${data}"`));
  195. }
  196. });
  197. });
  198. req.on('timeout', () => {
  199. req.destroy(new error_1.MongoAWSError(`AWS request to ${uri} timed out after ${options.timeout} ms`));
  200. });
  201. req.on('error', err => callback(err));
  202. req.end();
  203. }
  204. //# sourceMappingURL=mongodb_aws.js.map