Repository: aws/aws-sdk-js
Pull request #1813 (2017-11-29 00:09:13)

Add AWS.RemoteCredentials as an alias of ECSCredentials
This PR incorporates #1812 and modifies it to alias `AWS.ECSCredentials` to `AWS.RemoteCredentials` rather than having the former extend the latter.


Patch:
diff --git a/.changes/next-release/feature-credentials_provider-683a1b9e.json b/.changes/next-release/feature-credentials_provider-683a1b9e.json
new file mode 100644
index 000000000..8c265d8c1
--- /dev/null
+++ b/.changes/next-release/feature-credentials_provider-683a1b9e.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "credentials_provider",
+  "description": "Add general purpose RemoteCredentials and derive ECSCredentials from it"
+}
\ No newline at end of file
diff --git a/lib/core.d.ts b/lib/core.d.ts
index 1cd8f7f47..427a4be8c 100644
--- a/lib/core.d.ts
+++ b/lib/core.d.ts
@@ -3,6 +3,7 @@ export {Credentials} from './credentials';
 export {CognitoIdentityCredentials} from './credentials/cognito_identity_credentials';
 export {CredentialProviderChain} from './credentials/credential_provider_chain';
 export {EC2MetadataCredentials} from './credentials/ec2_metadata_credentials';
+export {RemoteCredentials} from './credentials/remote_credentials';
 export {ECSCredentials} from './credentials/ecs_credentials';
 export {EnvironmentCredentials} from './credentials/environment_credentials';
 export {FileSystemCredentials} from './credentials/file_system_credentials';
diff --git a/lib/credentials/ecs_credentials.d.ts b/lib/credentials/ecs_credentials.d.ts
index 741e8223f..bf30eff30 100644
--- a/lib/credentials/ecs_credentials.d.ts
+++ b/lib/credentials/ecs_credentials.d.ts
@@ -1,11 +1,11 @@
-import {Credentials} from '../credentials';
-export class ECSCredentials extends Credentials {
-		/**
-		 * Represents credentials received from relative URI specified in the ECS container.
-		 * @param {object} options - Override the default (1s) timeout period.
-		 */
-		constructor(options?: ECSCredentialsOptions);
-	}
+import {RemoteCredentials} from './remote_credentials';
+export class ECSCredentials extends RemoteCredentials {
+        /**
+         * Represents credentials received.
+         * @param {object} options - Override the default (1s) timeout period.
+         */
+        constructor(options?: ECSCredentialsOptions);
+    }
     interface ECSCredentialsOptions {
         httpOptions?: {
             /**
diff --git a/lib/credentials/ecs_credentials.js b/lib/credentials/ecs_credentials.js
index b52a9bdde..d5a035a95 100644
--- a/lib/credentials/ecs_credentials.js
+++ b/lib/credentials/ecs_credentials.js
@@ -1,11 +1,4 @@
-var AWS = require('../core'),
-  ENV_RELATIVE_URI = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
-  ENV_FULL_URI = 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
-  ENV_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN',
-  FULL_URI_UNRESTRICTED_PROTOCOLS = ['https:'],
-  FULL_URI_ALLOWED_PROTOCOLS = ['http:', 'https:'],
-  FULL_URI_ALLOWED_HOSTNAMES = ['localhost', '127.0.0.1'],
-  RELATIVE_URI_HOST = '169.254.170.2';
+var AWS = require('../core');
 
 /**
  * Represents credentials received from relative URI specified in the ECS container.
@@ -32,188 +25,4 @@ var AWS = require('../core'),
  *
  * @!macro nobrowser
  */
-AWS.ECSCredentials = AWS.util.inherit(AWS.Credentials, {
-  constructor: function ECSCredentials(options) {
-    AWS.Credentials.call(this);
-    options = options ? AWS.util.copy(options) : {};
-    if (!options.httpOptions) options.httpOptions = {};
-    options.httpOptions = AWS.util.merge(
-      this.httpOptions, options.httpOptions);
-    AWS.util.update(this, options);
-  },
-
-  /**
-   * @api private
-   */
-  httpOptions: { timeout: 1000 },
-
-  /**
-   * @api private
-   */
-  maxRetries: 3,
-
-  /**
-   * @api private
-   */
-  isConfiguredForEcsCredentials: function isConfiguredForEcsCredentials() {
-    return Boolean(
-        process &&
-        process.env &&
-        (process.env[ENV_RELATIVE_URI] || process.env[ENV_FULL_URI])
-    );
-  },
-
-  /**
-   * @api private
-   */
-  getECSFullUri: function getECSFullUri() {
-    if (process && process.env) {
-      var relative = process.env[ENV_RELATIVE_URI],
-          full = process.env[ENV_FULL_URI];
-      if (relative) {
-        return 'http://' + RELATIVE_URI_HOST + relative;
-      } else if (full) {
-        var parsed = AWS.util.urlParse(full);
-        if (FULL_URI_ALLOWED_PROTOCOLS.indexOf(parsed.protocol) < 0) {
-          throw AWS.util.error(
-            new Error('Unsupported protocol:  AWS.ECSCredentials supports '
-              + FULL_URI_ALLOWED_PROTOCOLS.join(',') + ' only; '
-              + parsed.protocol + ' requested.'),
-            { code: 'ECSCredentialsProviderFailure' }
-          );
-        }
-
-        if (FULL_URI_UNRESTRICTED_PROTOCOLS.indexOf(parsed.protocol) < 0 &&
-            FULL_URI_ALLOWED_HOSTNAMES.indexOf(parsed.hostname) < 0) {
-          throw AWS.util.error(
-            new Error('Unsupported hostname: AWS.ECSCredentials only supports '
-              + FULL_URI_ALLOWED_HOSTNAMES.join(',') + ' for ' + parsed.protocol + '; '
-              + parsed.protocol + parsed.hostname + ' requested.'),
-            { code: 'ECSCredentialsProviderFailure' }
-          );
-        }
-
-        return full;
-      }
-    }
-  },
-
-  /**
-   * @api private
-   */
-  getECSAuthToken: function getECSAuthToken() {
-    if (process && process.env && process.env[ENV_FULL_URI]) {
-      return process.env[ENV_AUTH_TOKEN];
-    }
-  },
-
-  /**
-   * @api private
-   */
-  credsFormatIsValid: function credsFormatIsValid(credData) {
-    return (!!credData.accessKeyId && !!credData.secretAccessKey &&
-      !!credData.sessionToken && !!credData.expireTime);
-  },
-
-  /**
-   * @api private
-   */
-  formatCreds: function formatCreds(credData) {
-    if (!!credData.credentials) {
-      credData = credData.credentials;
-    }
-
-    return {
-      expired: false,
-      accessKeyId: credData.accessKeyId || credData.AccessKeyId,
-      secretAccessKey: credData.secretAccessKey || credData.SecretAccessKey,
-      sessionToken: credData.sessionToken || credData.Token,
-      expireTime: new Date(credData.expiration || credData.Expiration)
-    };
-  },
-
-  /**
-   * @api private
-   */
-  request: function request(url, callback) {
-    var httpRequest = new AWS.HttpRequest(url);
-    httpRequest.method = 'GET';
-    httpRequest.headers.Accept = 'application/json';
-    var token = this.getECSAuthToken();
-    if (token) {
-      httpRequest.headers.Authorization = token;
-    }
-    AWS.util.handleRequestWithRetries(httpRequest, this, callback);
-  },
-
-  /**
-   * @api private
-   */
-  refreshQueue: [],
-
-  /**
-   * Loads the credentials from the relative URI specified by container
-   *
-   * @callback callback function(err)
-   *   Called when the request to the relative URI responds (or fails). When
-   *   this callback is called with no error, it means that the credentials
-   *   information has been loaded into the object (as the `accessKeyId`,
-   *   `secretAccessKey`, `sessionToken`, and `expireTime` properties).
-   *   @param err [Error] if an error occurred, this value will be filled
-   * @see get
-   */
-  refresh: function refresh(callback) {
-    var self = this;
-    var refreshQueue = self.refreshQueue;
-    if (!callback) callback = function(err) { if (err) throw err; };
-    refreshQueue.push({
-      provider: self,
-      errCallback: callback
-    });
-    if (refreshQueue.length > 1) { return; }
-
-    function callbacks(err, creds) {
-      var call, cb;
-      while ((call = refreshQueue.shift()) !== undefined) {
-        cb = call.errCallback;
-        if (!err) AWS.util.update(call.provider, creds);
-        cb(err);
-      }
-    }
-
-    if (process === undefined) {
-      callbacks(AWS.util.error(
-        new Error('No process info available'),
-        { code: 'ECSCredentialsProviderFailure' }
-      ));
-      return;
-    }
-    var fullUri = this.getECSFullUri();
-    if (fullUri === undefined) {
-      callbacks(AWS.util.error(
-        new Error('Variable ' + ENV_RELATIVE_URI + ' or ' + ENV_FULL_URI +
-          ' must be set to use AWS.ECSCredentials.'),
-        { code: 'ECSCredentialsProviderFailure' }
-      ));
-      return;
-    }
-
-    this.request(fullUri, function(err, data) {
-      if (!err) {
-        try {
-          data = JSON.parse(data);
-          var creds = self.formatCreds(data);
-          if (!self.credsFormatIsValid(creds)) {
-            throw AWS.util.error(
-              new Error('Response data is not in valid format'),
-              { code: 'ECSCredentialsProviderFailure' }
-            );
-          }
-        } catch (dataError) {
-          err = dataError;
-        }
-      }
-      callbacks(err, creds);
-    });
-  }
-});
+AWS.ECSCredentials = AWS.RemoteCredentials;
diff --git a/lib/credentials/remote_credentials.d.ts b/lib/credentials/remote_credentials.d.ts
new file mode 100644
index 000000000..dd91892b3
--- /dev/null
+++ b/lib/credentials/remote_credentials.d.ts
@@ -0,0 +1,17 @@
+import {Credentials} from '../credentials';
+export class RemoteCredentials extends Credentials {
+        /**
+         * Represents credentials received.
+         * @param {object} options - Override the default (1s) timeout period.
+         */
+        constructor(options?: RemoteCredentialsOptions);
+    }
+    interface RemoteCredentialsOptions {
+        httpOptions?: {
+            /**
+             * Timeout in milliseconds.
+             */
+            timeout?: number
+        }
+        maxRetries?: number
+    }
\ No newline at end of file
diff --git a/lib/credentials/remote_credentials.js b/lib/credentials/remote_credentials.js
new file mode 100644
index 000000000..0fd534da7
--- /dev/null
+++ b/lib/credentials/remote_credentials.js
@@ -0,0 +1,219 @@
+var AWS = require('../core'),
+  ENV_RELATIVE_URI = 'AWS_CONTAINER_CREDENTIALS_RELATIVE_URI',
+  ENV_FULL_URI = 'AWS_CONTAINER_CREDENTIALS_FULL_URI',
+  ENV_AUTH_TOKEN = 'AWS_CONTAINER_AUTHORIZATION_TOKEN',
+  FULL_URI_UNRESTRICTED_PROTOCOLS = ['https:'],
+  FULL_URI_ALLOWED_PROTOCOLS = ['http:', 'https:'],
+  FULL_URI_ALLOWED_HOSTNAMES = ['localhost', '127.0.0.1'],
+  RELATIVE_URI_HOST = '169.254.170.2';
+
+/**
+ * Represents credentials received from specified URI.
+ *
+ * This class will request refreshable credentials from the relative URI
+ * specified by the AWS_REMOTE_CREDENTIALS_RELATIVE_URI or the
+ * AWS_REMOTE_CREDENTIALS_FULL_URI environment variable. If valid credentials
+ * are returned in the response, these will be used with zero configuration.
+ *
+ * This credentials class will by default timeout after 1 second of inactivity
+ * and retry 3 times.
+ * If your requests to the relative URI are timing out, you can increase
+ * the value by configuring them directly:
+ *
+ * ```javascript
+ * AWS.config.credentials = new AWS.RemoteCredentials({
+ *   httpOptions: { timeout: 5000 }, // 5 second timeout
+ *   maxRetries: 10, // retry 10 times
+ *   retryDelayOptions: { base: 200 } // see AWS.Config for information
+ * });
+ * ```
+ *
+ * @see AWS.Config.retryDelayOptions
+ *
+ * @!macro nobrowser
+ */
+AWS.RemoteCredentials = AWS.util.inherit(AWS.Credentials, {
+  constructor: function RemoteCredentials(options) {
+    AWS.Credentials.call(this);
+    options = options ? AWS.util.copy(options) : {};
+    if (!options.httpOptions) options.httpOptions = {};
+    options.httpOptions = AWS.util.merge(
+      this.httpOptions, options.httpOptions);
+    AWS.util.update(this, options);
+  },
+
+  /**
+   * @api private
+   */
+  httpOptions: { timeout: 1000 },
+
+  /**
+   * @api private
+   */
+  maxRetries: 3,
+
+  /**
+   * @api private
+   */
+  isConfiguredForEcsCredentials: function isConfiguredForEcsCredentials() {
+    return Boolean(
+        process &&
+        process.env &&
+        (process.env[ENV_RELATIVE_URI] || process.env[ENV_FULL_URI])
+    );
+  },
+
+  /**
+   * @api private
+   */
+  getECSFullUri: function getECSFullUri() {
+    if (process && process.env) {
+      var relative = process.env[ENV_RELATIVE_URI],
+          full = process.env[ENV_FULL_URI];
+      if (relative) {
+        return 'http://' + RELATIVE_URI_HOST + relative;
+      } else if (full) {
+        var parsed = AWS.util.urlParse(full);
+        if (FULL_URI_ALLOWED_PROTOCOLS.indexOf(parsed.protocol) < 0) {
+          throw AWS.util.error(
+            new Error('Unsupported protocol:  AWS.RemoteCredentials supports '
+              + FULL_URI_ALLOWED_PROTOCOLS.join(',') + ' only; '
+              + parsed.protocol + ' requested.'),
+            { code: 'ECSCredentialsProviderFailure' }
+          );
+        }
+
+        if (FULL_URI_UNRESTRICTED_PROTOCOLS.indexOf(parsed.protocol) < 0 &&
+            FULL_URI_ALLOWED_HOSTNAMES.indexOf(parsed.hostname) < 0) {
+          throw AWS.util.error(
+            new Error('Unsupported hostname: AWS.RemoteCredentials only supports '
+              + FULL_URI_ALLOWED_HOSTNAMES.join(',') + ' for ' + parsed.protocol + '; '
+              + parsed.protocol + '//' + parsed.hostname + ' requested.'),
+            { code: 'ECSCredentialsProviderFailure' }
+          );
+        }
+
+        return full;
+      }
+    }
+  },
+
+  /**
+   * @api private
+   */
+  getECSAuthToken: function getECSAuthToken() {
+    if (process && process.env && process.env[ENV_FULL_URI]) {
+      return process.env[ENV_AUTH_TOKEN];
+    }
+  },
+
+  /**
+   * @api private
+   */
+  credsFormatIsValid: function credsFormatIsValid(credData) {
+    return (!!credData.accessKeyId && !!credData.secretAccessKey &&
+      !!credData.sessionToken && !!credData.expireTime);
+  },
+
+  /**
+   * @api private
+   */
+  formatCreds: function formatCreds(credData) {
+    if (!!credData.credentials) {
+      credData = credData.credentials;
+    }
+
+    return {
+      expired: false,
+      accessKeyId: credData.accessKeyId || credData.AccessKeyId,
+      secretAccessKey: credData.secretAccessKey || credData.SecretAccessKey,
+      sessionToken: credData.sessionToken || credData.Token,
+      expireTime: new Date(credData.expiration || credData.Expiration)
+    };
+  },
+
+  /**
+   * @api private
+   */
+  request: function request(url, callback) {
+    var httpRequest = new AWS.HttpRequest(url);
+    httpRequest.method = 'GET';
+    httpRequest.headers.Accept = 'application/json';
+    var token = this.getECSAuthToken();
+    if (token) {
+      httpRequest.headers.Authorization = token;
+    }
+    AWS.util.handleRequestWithRetries(httpRequest, this, callback);
+  },
+
+  /**
+   * @api private
+   */
+  refreshQueue: [],
+
+  /**
+   * Loads the credentials from the relative URI specified by container
+   *
+   * @callback callback function(err)
+   *   Called when the request to the relative URI responds (or fails). When
+   *   this callback is called with no error, it means that the credentials
+   *   information has been loaded into the object (as the `accessKeyId`,
+   *   `secretAccessKey`, `sessionToken`, and `expireTime` properties).
+   *   @param err [Error] if an error occurred, this value will be filled
+   * @see get
+   */
+  refresh: function refresh(callback) {
+    var self = this;
+    var refreshQueue = self.refreshQueue;
+    if (!callback) callback = function(err) { if (err) throw err; };
+    refreshQueue.push({
+      provider: self,
+      errCallback: callback
+    });
+    if (refreshQueue.length > 1) { return; }
+
+    function callbacks(err, creds) {
+      var call, cb;
+      while ((call = refreshQueue.shift()) !== undefined) {
+        cb = call.errCallback;
+        if (!err) AWS.util.update(call.provider, creds);
+        cb(err);
+      }
+    }
+
+    if (process === undefined) {
+      callbacks(AWS.util.error(
+        new Error('No process info available'),
+        { code: 'ECSCredentialsProviderFailure' }
+      ));
+      return;
+    }
+    var fullUri = this.getECSFullUri();
+    if (fullUri === undefined) {
+      callbacks(AWS.util.error(
+        new Error('Variable ' + ENV_RELATIVE_URI + ' or ' + ENV_FULL_URI +
+          ' must be set to use AWS.RemoteCredentials.'),
+        { code: 'ECSCredentialsProviderFailure' }
+      ));
+      return;
+    }
+
+    this.request(fullUri, function(err, data) {
+      if (!err) {
+        try {
+          data = JSON.parse(data);
+          var creds = self.formatCreds(data);
+          if (!self.credsFormatIsValid(creds)) {
+            throw AWS.util.error(
+              new Error('Response data is not in valid format'),
+              { code: 'ECSCredentialsProviderFailure' }
+            );
+          }
+        } catch (dataError) {
+          err = dataError;
+        }
+      }
+      callbacks(err, creds);
+    });
+  }
+});
diff --git a/lib/node_loader.js b/lib/node_loader.js
index 0e60e0534..23f1bf17e 100644
--- a/lib/node_loader.js
+++ b/lib/node_loader.js
@@ -26,6 +26,7 @@ require('./http/node');
 
 // Load custom credential providers
 require('./credentials/ec2_metadata_credentials');
+require('./credentials/remote_credentials');
 require('./credentials/ecs_credentials');
 require('./credentials/environment_credentials');
 require('./credentials/file_system_credentials');
diff --git a/package.json b/package.json
index b97522997..db58946fa 100644
--- a/package.json
+++ b/package.json
@@ -138,4 +138,4 @@
     "build-react-native": "npm -s run-script build-react-native-deps && npm -s run-script build-react-native-core && npm -s run-script build-react-native-dist",
     "react-native-test": "npm -s run-script build-react-native && rake reactnative:test && karma start"
   }
-}
\ No newline at end of file
+}
diff --git a/ts/core.ts b/ts/core.ts
index c0f4503e2..ec71ba050 100644
--- a/ts/core.ts
+++ b/ts/core.ts
@@ -11,6 +11,8 @@ if (core.CognitoIdentityCredentials) {
 
 } else if (core.EC2MetadataCredentials) {
 
+} else if (core.RemoteCredentials) {
+
 } else if (core.ECSCredentials) {
 
 } else if (core.Endpoint) {


Test patch:
diff --git a/test/credentials.spec.js b/test/credentials.spec.js
index 9d72b59ca..8347d968d 100644
--- a/test/credentials.spec.js
+++ b/test/credentials.spec.js
@@ -601,7 +601,7 @@
         });
       });
     });
-    describe('AWS.ECSCredentials', function() {
+    describe('AWS.RemoteCredentials', function() {
       var creds, mockEndpoint, responseData, responseDataNew;
       creds = null;
       responseData = {
@@ -619,7 +619,7 @@
         }
       };
       beforeEach(function() {
-        creds = new AWS.ECSCredentials({
+        creds = new AWS.RemoteCredentials({
           host: 'host'
         });
         return process.env = {};
@@ -636,6 +636,7 @@
           })));
         });
       };
+
       describe('constructor', function() {
         it('allows passing of options', function() {
           return expect(creds.host).to.equal('host');
@@ -643,7 +644,7 @@
         it('does not modify options object', function() {
           var opts;
           opts = {};
-          creds = new AWS.ECSCredentials(opts);
+          creds = new AWS.RemoteCredentials(opts);
           return expect(opts).to.eql({});
         });
         return it('allows setting timeout', function() {
@@ -653,12 +654,12 @@
               timeout: 5000
             }
           };
-          creds = new AWS.ECSCredentials(opts);
+          creds = new AWS.RemoteCredentials(opts);
           return expect(creds.httpOptions.timeout).to.equal(5000);
         });
       });
 
-      describe('isConfiguredForEcsCredentials', function () {
+      describe('isConfiguredForRemoteCredentials', function () {
         it('returns false when process is not available', function() {
           var process_copy = process;
           process = void 0;
@@ -693,7 +694,7 @@
           'returns true from the object prototype when the relative URI environment variable is set',
           function() {
             process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
-            expect(AWS.ECSCredentials.prototype.isConfiguredForEcsCredentials())
+            expect(AWS.RemoteCredentials.prototype.isConfiguredForEcsCredentials())
               .to.equal(true);
           }
         );
@@ -702,13 +703,13 @@
           'returns true from the object prototype when the full URI environment variable is set',
           function() {
             process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://localhost/get-credentials';
-            expect(AWS.ECSCredentials.prototype.isConfiguredForEcsCredentials())
+            expect(AWS.RemoteCredentials.prototype.isConfiguredForEcsCredentials())
               .to.equal(true);
           }
         );
       });
 
-      describe('getECSFullUri', function() {
+      describe('getRemoteFullUri', function() {
         it('returns undefined when process is not available', function() {
           var process, process_copy;
           process_copy = process;
@@ -833,6 +834,285 @@
         });
       });
 
+      describe('refresh', function() {
+        it('loads credentials from specified relative URI', function() {
+          var callbackErr = null;
+          process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+          var spy = mockEndpoint(new Date(AWS.util.date.getDate().getTime() + 100000));
+          creds.refresh(function(err) {
+            callbackErr = err;
+          });
+          expect(spy.calls.length).to.equal(1);
+          expect(spy.calls[0].arguments[0])
+            .to.equal('http://169.254.170.2/path');
+          expect(callbackErr).to.be["null"];
+          expect(creds.accessKeyId).to.equal('KEY');
+          expect(creds.secretAccessKey).to.equal('SECRET');
+          expect(creds.sessionToken).to.equal('TOKEN');
+          expect(creds.needsRefresh()).to.equal(false);
+        });
+
+        it('loads credentials from specified full URI', function() {
+          var callbackErr = null;
+          process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://localhost/get-credentials';
+          var spy = mockEndpoint(new Date(AWS.util.date.getDate().getTime() + 100000));
+          creds.refresh(function(err) {
+            callbackErr = err;
+          });
+          expect(spy.calls.length).to.equal(1);
+          expect(spy.calls[0].arguments[0])
+            .to.equal('http://localhost/get-credentials');
+          expect(callbackErr).to.be["null"];
+          expect(creds.accessKeyId).to.equal('KEY');
+          expect(creds.secretAccessKey).to.equal('SECRET');
+          expect(creds.sessionToken).to.equal('TOKEN');
+          expect(creds.needsRefresh()).to.equal(false);
+        });
+
+        it('passes an error to the callback when environment variable not set', function() {
+          var callbackErr, spy;
+          callbackErr = null;
+          spy = mockEndpoint(new Date(AWS.util.date.getDate().getTime() + 100000));
+          creds.refresh(function(err) {
+            return callbackErr = err;
+          });
+          expect(spy.calls.length).to.equal(0);
+          expect(callbackErr).to.not.be["null"];
+        });
+
+        it('retries up to specified maxRetries for timeout errors', function(done) {
+          var httpClient, options, spy;
+          process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+          options = {
+            maxRetries: 3
+          };
+          creds = new AWS.RemoteCredentials(options);
+          httpClient = AWS.HttpClient.getInstance();
+          spy = helpers.spyOn(httpClient, 'handleRequest').andCallFake(function(httpReq, httpOp, cb, errCb) {
+            return errCb({
+              code: 'TimeoutError'
+            });
+          });
+          creds.refresh(function(err) {
+            expect(err).to.not.be["null"];
+            expect(err.code).to.equal('TimeoutError');
+            expect(spy.calls.length).to.equal(4);
+            done();
+          });
+        });
+
+        it('passes the environmental auth token to the request', function(done) {
+          process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://localhost/get-credentials';
+          process.env['AWS_CONTAINER_AUTHORIZATION_TOKEN'] = 'Basic abcd';
+          creds = new AWS.RemoteCredentials();
+          var httpClient = AWS.HttpClient.getInstance();
+          helpers.spyOn(httpClient, 'handleRequest').andCallFake(function(httpReq, httpOp, cb, errCb) {
+            expect(httpReq.headers.Authorization).to.equal('Basic abcd');
+            helpers.mockHttpSuccessfulResponse(200, {}, JSON.stringify(responseData), cb);
+          });
+          creds.refresh(function(err) {
+            expect(err).to.be["null"];
+            done();
+          });
+        });
+
+        it('makes only one request when multiple calls are made before first one finishes', function(done) {
+          var callRefresh, concurrency, countdown, j, providers, ref, results, spy, x;
+          concurrency = countdown = 10;
+          process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+          spy = helpers.spyOn(AWS.RemoteCredentials.prototype, 'request').andCallFake(function(path, cb) {
+            var respond;
+            respond = function() {
+              return cb(null, JSON.stringify(responseData));
+            };
+            return process.nextTick(respond);
+          });
+          providers = [];
+          callRefresh = function(ind) {
+            providers[ind] = new AWS.RemoteCredentials({
+              host: 'host'
+            });
+            return providers[ind].refresh(function(err) {
+              expect(err).to.equal(null);
+              expect(providers[ind].accessKeyId).to.equal('KEY');
+              countdown--;
+              if (countdown === 0) {
+                expect(spy.calls.length).to.equal(1);
+                return done();
+              }
+            });
+          };
+          results = [];
+          for (x = j = 1, ref = concurrency; 1 <= ref ? j <= ref : j >= ref; x = 1 <= ref ? ++j : --j) {
+            results.push(callRefresh(x - 1));
+          }
+          return results;
+        });
+      });
+    });
+    describe('AWS.ECSCredentials', function() {
+      var creds, mockEndpoint, responseData, responseDataNew;
+      creds = null;
+      responseData = {
+        AccessKeyId: 'KEY',
+        SecretAccessKey: 'SECRET',
+        Token: 'TOKEN',
+        Expiration: (new Date(0)).toISOString()
+      };
+      responseDataNew = {
+        credentials: {
+          accessKeyId: 'KEY',
+          secretAccessKey: 'SECRET',
+          sessionToken: 'TOKEN',
+          expiration: (new Date(0)).toISOString()
+        }
+      };
+      beforeEach(function() {
+        creds = new AWS.ECSCredentials({
+          host: 'host'
+        });
+        return process.env = {};
+      });
+      afterEach(function() {
+        return process.env = {};
+      });
+      mockEndpoint = function(expireTime) {
+        return helpers.spyOn(creds, 'request').andCallFake(function(path, cb) {
+          var expiration;
+          expiration = expireTime.toISOString();
+          return cb(null, JSON.stringify(AWS.util.merge(responseData, {
+            Expiration: expiration
+          })));
+        });
+      };
+
+      describe('constructor', function() {
+        it('allows passing of options', function() {
+          return expect(creds.host).to.equal('host');
+        });
+        it('does not modify options object', function() {
+          var opts;
+          opts = {};
+          creds = new AWS.ECSCredentials(opts);
+          return expect(opts).to.eql({});
+        });
+        return it('allows setting timeout', function() {
+          var opts;
+          opts = {
+            httpOptions: {
+              timeout: 5000
+            }
+          };
+          creds = new AWS.ECSCredentials(opts);
+          return expect(creds.httpOptions.timeout).to.equal(5000);
+        });
+      });
+
+      describe('isConfiguredForRemoteCredentials', function () {
+        it('returns false when process is not available', function() {
+          var process_copy = process;
+          process = void 0;
+          expect(creds.isConfiguredForEcsCredentials()).to.equal(false);
+          process = process_copy;
+        });
+
+        it(
+          'returns false when relative URI environment variable not set',
+          function() {
+            expect(creds.isConfiguredForEcsCredentials()).to.equal(false);
+          }
+        );
+
+        it(
+          'returns true when the relative URI environment variable is set',
+          function() {
+            process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+            expect(creds.isConfiguredForEcsCredentials()).to.equal(true);
+          }
+        );
+
+        it(
+          'returns true when the full URI environment variable is set',
+          function() {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://localhost/get-credentials';
+            expect(creds.isConfiguredForEcsCredentials()).to.equal(true);
+          }
+        );
+
+      });
+
+      describe('getRemoteFullUri', function() {
+        it('returns undefined when process is not available', function() {
+          var process, process_copy;
+          process_copy = process;
+          process = void 0;
+          expect(creds.getECSFullUri()).to.equal(void 0);
+          process = process_copy;
+        });
+
+        it(
+          'returns undefined when neither the relative URI environment variable nor the full URI environment variable is set',
+          function() {
+            expect(creds.getECSFullUri()).to.equal(void 0);
+          }
+        );
+
+        it(
+          'returns a full URI when the relative URI environment variable is set',
+          function() {
+            process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+            expect(creds.getECSFullUri()).to.equal('http://169.254.170.2/path');
+          }
+        );
+
+        it(
+          'returns a full URI when the full URI environment variable is set',
+          function() {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://localhost/get-credentials';
+            expect(creds.getECSFullUri())
+                .to.equal('http://localhost/get-credentials');
+          }
+        );
+
+        it(
+          'throws an error when the full URI environment variable contains a URI with an unsupported protocol',
+          function () {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'wss://localhost/get-credentials';
+            expect(creds.getECSFullUri.bind(creds))
+                .to.throw(/Unsupported protocol/);
+          }
+        );
+
+        it(
+          'throws an error when the full URI environment variable contains a URI with an unsupported protocol',
+          function () {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'http://s3-us-west-2.amazonaws.com/bucket/credentials';
+            expect(creds.getECSFullUri.bind(creds))
+              .to.throw(/Unsupported hostname/);
+          }
+        );
+
+        it(
+          'returns a full URI when the full URI environment variable is set to a non-localhost https URI',
+          function () {
+            process.env['AWS_CONTAINER_CREDENTIALS_FULL_URI'] = 'https://s3-us-west-2.amazonaws.com/bucket/credentials';
+            expect(creds.getECSFullUri())
+              .to.equal('https://s3-us-west-2.amazonaws.com/bucket/credentials');
+          }
+        );
+      });
+
+      describe('needsRefresh', function() {
+        return it('can be expired based on expire time from URI endpoint', function() {
+          var spy;
+          process.env['AWS_CONTAINER_CREDENTIALS_RELATIVE_URI'] = '/path';
+          spy = mockEndpoint(new Date(0));
+          creds.refresh(function() {});
+          expect(spy.calls.length).to.equal(1);
+          expect(creds.needsRefresh()).to.equal(true);
+        });
+      });
+
       describe('refresh', function() {
         it('loads credentials from specified relative URI', function() {
           var callbackErr = null;