Repository: aws/aws-sdk-js
Pull request #2838 (2019-09-05 20:46:06)

Add support opt-in regional STS endpoints
<!--
Thank you for your pull request. Please provide a description below.
-->

This PR makes STS client supports sending requests to regional endpoints(e.g. sts.us-west-2.amazonaws.com). You can opt in by setting `stsRegionalEndpoints` client config or `AWS_STS_REGIONAL_ENDPOINTS` environment or `sts_regional_endpoints` config file entry to 'regional'. 

##### Checklist
<!-- Remove items that do not apply. For completed items, change [ ] to [x]. -->

- [x] `npm run test` passes
- [x] `.d.ts` file is updated
- [x] changelog is added, `npm run add-change`
- [x] run `bundle exec rake docs:api` and inspect `doc/latest/index.html` if documentation is changed


codecov-io: # [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=h1) Report
> Merging [#2838](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=desc) into [master](https://codecov.io/gh/aws/aws-sdk-js/commit/88b635be22df3207ab7971d9b3192b5523a0a8b8?src=pr&el=desc) will **decrease** coverage by `0.01%`.
> The diff coverage is `93.33%`.

[![Impacted file tree graph](https://codecov.io/gh/aws/aws-sdk-js/pull/2838/graphs/tree.svg?width=650&token=qXdPGBZWAr&height=150&src=pr)](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master    #2838      +/-   ##
==========================================
- Coverage   96.93%   96.91%   -0.02%     
==========================================
  Files         297      297              
  Lines        8898     8943      +45     
  Branches     1663     1676      +13     
==========================================
+ Hits         8625     8667      +42     
- Misses        273      276       +3
```


| [Impacted Files](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [lib/config.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2838/diff?src=pr&el=tree#diff-bGliL2NvbmZpZy5qcw==) | `87.64% <ø> (ø)` | :arrow_up: |
| [lib/services/sts.js](https://codecov.io/gh/aws/aws-sdk-js/pull/2838/diff?src=pr&el=tree#diff-bGliL3NlcnZpY2VzL3N0cy5qcw==) | `95% <93.33%> (-5%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=footer). Last update [88b635b...7455f9a](https://codecov.io/gh/aws/aws-sdk-js/pull/2838?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/.changes/next-release/feature-Credentials-78e15543.json b/.changes/next-release/feature-Credentials-78e15543.json
new file mode 100644
index 000000000..e6bd7c788
--- /dev/null
+++ b/.changes/next-release/feature-Credentials-78e15543.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "Credentials",
+  "description": "Added chaining support in TokenFileWebIdentityCredentials"
+}
\ No newline at end of file
diff --git a/.changes/next-release/feature-Endpoints-5d7d9e4e.json b/.changes/next-release/feature-Endpoints-5d7d9e4e.json
new file mode 100644
index 000000000..a9ae5976a
--- /dev/null
+++ b/.changes/next-release/feature-Endpoints-5d7d9e4e.json
@@ -0,0 +1,5 @@
+{
+  "type": "feature",
+  "category": "Endpoints",
+  "description": "Sts client now supports sending requests to regional endpoints. You can opt in by setting stsRegionalEndpoints client config or AWS_STS_REGIONAL_ENDPOINTS environment or sts_regional_endpoints config file entry to 'regional'"
+}
\ No newline at end of file
diff --git a/lib/config.d.ts b/lib/config.d.ts
index 5edb827e4..77839d55b 100644
--- a/lib/config.d.ts
+++ b/lib/config.d.ts
@@ -291,4 +291,9 @@ export abstract class ConfigurationOptions {
      *  whether to marshal request parameters to the prefix of hostname.
      */
     hostPrefixEnabled?: boolean;
+    /**
+     * whether to send sts request to global endpoints or
+     * regional endpoints. 
+     */
+    stsRegionalEndpoints?: "legacy"|"regional";
 }
diff --git a/lib/config.js b/lib/config.js
index 59a64ade6..6484f072e 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -164,6 +164,11 @@ var PromisesDependency;
  * @!attribute hostPrefixEnabled
  *   @return [Boolean] whether to marshal request parameters to the prefix of
  *     hostname. Defaults to `true`.
+ *
+ * @!attribute stsRegionalEndpoints
+ *   @return ['legacy'|'regional'] whether to send sts request to global endpoints or
+ *     regional endpoints.
+ *     Defaults to 'legacy'
  */
 AWS.Config = AWS.util.inherit({
   /**
@@ -303,6 +308,9 @@ AWS.Config = AWS.util.inherit({
    * @option options hostPrefixEnabled [Boolean] whether to marshal request
    *   parameters to the prefix of hostname.
    *   Defaults to `true`.
+   * @option options stsRegionalEndpoints ['legacy'|'regional'] whether to send sts request
+   *   to global endpoints or regional endpoints.
+   *   Defaults to 'legacy'.
    */
   constructor: function Config(options) {
     if (options === undefined) options = {};
@@ -523,7 +531,8 @@ AWS.Config = AWS.util.inherit({
     clientSideMonitoring: false,
     endpointDiscoveryEnabled: false,
     endpointCacheSize: 1000,
-    hostPrefixEnabled: true
+    hostPrefixEnabled: true,
+    stsRegionalEndpoints: null
   },
 
   /**
diff --git a/lib/credentials/token_file_web_identity_credentials.js b/lib/credentials/token_file_web_identity_credentials.js
index c792afbe3..f8466a1f8 100644
--- a/lib/credentials/token_file_web_identity_credentials.js
+++ b/lib/credentials/token_file_web_identity_credentials.js
@@ -59,11 +59,11 @@ AWS.TokenFileWebIdentityCredentials = AWS.util.inherit(AWS.Credentials, {
     var ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE',
         ENV_ROLE_ARN = 'AWS_ROLE_ARN';
     if (process.env[ENV_TOKEN_FILE] && process.env[ENV_ROLE_ARN]) {
-      return {
+      return [{
         envTokenFile: process.env[ENV_TOKEN_FILE],
         roleArn: process.env[ENV_ROLE_ARN],
         roleSessionName: process.env['AWS_ROLE_SESSION_NAME']
-      };
+      }];
     }
   },
 
@@ -84,11 +84,24 @@ AWS.TokenFileWebIdentityCredentials = AWS.util.inherit(AWS.Credentials, {
       );
     }
 
-    return {
+    var paramsArray = [];
+
+    while (!profile['web_identity_token_file'] && profile['source_profile']) {
+      paramsArray.unshift({
+        roleArn: profile['role_arn'],
+        roleSessionName: profile['role_session_name']
+      });
+      var sourceProfile = profile['source_profile'];
+      profile = profiles[sourceProfile];
+    }
+
+    paramsArray.unshift({
       envTokenFile: profile['web_identity_token_file'],
       roleArn: profile['role_arn'],
       roleSessionName: profile['role_session_name']
-    };
+    });
+
+    return paramsArray;
   },
 
   /**
@@ -106,35 +119,67 @@ AWS.TokenFileWebIdentityCredentials = AWS.util.inherit(AWS.Credentials, {
     this.coalesceRefresh(callback || AWS.util.fn.callback);
   },
 
+  /**
+   * @api private
+  */
+  assumeRoleChaining: function assumeRoleChaining(paramsArray, callback) {
+    var self = this;
+    if (paramsArray.length === 0) {
+      self.service.credentialsFrom(self.data, self);
+      callback();
+    } else {
+      var params = paramsArray.shift();
+      self.service.config.credentials = self.service.credentialsFrom(self.data, self);
+      self.service.assumeRole(
+        {
+          RoleArn: params.roleArn,
+          RoleSessionName: params.roleSessionName || 'token-file-web-identity'
+        },
+        function (err, data) {
+          self.data = null;
+          if (err) {
+            callback(err);
+          } else {
+            self.data = data;
+            self.assumeRoleChaining(paramsArray, callback);
+          }
+        }
+      );
+    }
+  },
+
   /**
    * @api private
    */
   load: function load(callback) {
     var self = this;
     try {
-      var params = self.getParamsFromEnv();
-      if (!params) {
-        params = self.getParamsFromSharedConfig();
+      var paramsArray = self.getParamsFromEnv();
+      if (!paramsArray) {
+        paramsArray = self.getParamsFromSharedConfig();
       }
-      if (params) {
+      if (paramsArray) {
+        var params = paramsArray.shift();
         var oidcToken = fs.readFileSync(params.envTokenFile, {encoding: 'ascii'});
-        if (self.service) {
-          self.service.config.params.WebIdentityToken = oidcToken;
-        } else {
-          self.createClients({
+        if (!self.service) {
+          self.createClients();
+        }
+        self.service.assumeRoleWithWebIdentity(
+          {
             WebIdentityToken: oidcToken,
             RoleArn: params.roleArn,
             RoleSessionName: params.roleSessionName || 'token-file-web-identity'
-          });
-        }
-        self.service.assumeRoleWithWebIdentity(function (err, data) {
-          self.data = null;
-          if (!err) {
-            self.data = data;
-            self.service.credentialsFrom(data, self);
+          },
+          function (err, data) {
+            self.data = null;
+            if (err) {
+              callback(err);
+            } else {
+              self.data = data;
+              self.assumeRoleChaining(paramsArray, callback);
+            }
           }
-          callback(err);
-        });
+        );
       }
     } catch (err) {
       callback(err);
@@ -144,10 +189,9 @@ AWS.TokenFileWebIdentityCredentials = AWS.util.inherit(AWS.Credentials, {
   /**
    * @api private
    */
-  createClients: function(params) {
+  createClients: function() {
     if (!this.service) {
       var stsConfig = AWS.util.merge({}, this.clientConfig);
-      stsConfig.params = params;
       this.service = new STS(stsConfig);
 
       // Retry in case of IDPCommunicationErrorException or InvalidIdentityToken
diff --git a/lib/services/sts.js b/lib/services/sts.js
index 6e9fd00c8..6447159e3 100644
--- a/lib/services/sts.js
+++ b/lib/services/sts.js
@@ -1,4 +1,7 @@
 var AWS = require('../core');
+var regionConfig = require('../region_config');
+var ENV_REGIONAL_ENDPOINT_ENABLED = 'AWS_STS_REGIONAL_ENDPOINTS';
+var CONFIG_REGIONAL_ENDPOINT_ENABLED = 'sts_regional_endpoints';
 
 AWS.util.update(AWS.STS.prototype, {
   /**
@@ -43,5 +46,82 @@ AWS.util.update(AWS.STS.prototype, {
 
   assumeRoleWithSAML: function assumeRoleWithSAML(params, callback) {
     return this.makeUnauthenticatedRequest('assumeRoleWithSAML', params, callback);
+  },
+
+  /**
+   * @api private
+   */
+  validateRegionalEndpointsFlagValue: function validateRegionalEndpointsFlagValue(configValue, errorOptions) {
+    if (typeof configValue === 'string' && ['legacy', 'regional'].indexOf(configValue.toLowerCase()) >= 0) {
+      this.config.stsRegionalEndpoints = configValue.toLowerCase();
+      return;
+    } else {
+      throw AWS.util.error(new Error(), errorOptions);
+    }
+  },
+
+  /**
+   * @api private
+   */
+  validateRegionalEndpointsFlag: function validateRegionalEndpointsFlag() {
+    //validate config value
+    var config = this.config;
+    if (config.stsRegionalEndpoints) {
+      this.validateRegionalEndpointsFlagValue(config.stsRegionalEndpoints, {
+        code: 'InvalidConfiguration',
+        message: 'invalid "stsRegionalEndpoints" configuration. Expect "legacy" ' +
+        ' or "regional". Got "' + config.stsRegionalEndpoints + '".'
+      });
+    }
+    if (!AWS.util.isNode()) return;
+    //validate environmental variable
+    if (Object.prototype.hasOwnProperty.call(process.env, ENV_REGIONAL_ENDPOINT_ENABLED)) {
+      var envFlag = process.env[ENV_REGIONAL_ENDPOINT_ENABLED];
+      this.validateRegionalEndpointsFlagValue(envFlag, {
+        code: 'InvalidEnvironmentalVariable',
+        message: 'invalid ' + ENV_REGIONAL_ENDPOINT_ENABLED + ' environmental variable. Expect "legacy" ' +
+        ' or "regional". Got "' + process.env[ENV_REGIONAL_ENDPOINT_ENABLED] + '".'
+      });
+    }
+    //validate shared config file
+    var profile = {};
+    try {
+      var profiles = AWS.util.getProfilesFromSharedConfig(AWS.util.iniLoader);
+      profile = profiles[process.env.AWS_PROFILE || AWS.util.defaultProfile];
+    } catch (e) {};
+    if (Object.prototype.hasOwnProperty.call(profile, CONFIG_REGIONAL_ENDPOINT_ENABLED)) {
+      var fileFlag = profile[CONFIG_REGIONAL_ENDPOINT_ENABLED];
+      this.validateRegionalEndpointsFlagValue(fileFlag, {
+        code: 'InvalidConfiguration',
+        message: 'invalid '+CONFIG_REGIONAL_ENDPOINT_ENABLED+' profile config. Expect "legacy" ' +
+        ' or "regional". Got "' + profile[CONFIG_REGIONAL_ENDPOINT_ENABLED] + '".'
+      });
+    }
+  },
+
+  /**
+   * @api private
+   */
+  optInRegionalEndpoint: function optInRegionalEndpoint() {
+    this.validateRegionalEndpointsFlag();
+    var config = this.config;
+    if (config.stsRegionalEndpoints === 'regional') {
+      regionConfig(this);
+      if (!this.isGlobalEndpoint) return;
+      this.isGlobalEndpoint = false;
+      //client will throw if region is not supplied; request will be signed with specified region
+      if (!config.region) {
+        throw AWS.util.error(new Error(),
+          {code: 'ConfigError', message: 'Missing region in config'});
+      }
+      var insertPoint = config.endpoint.indexOf('.amazonaws.com');
+      config.endpoint = config.endpoint.substring(0, insertPoint) +
+        '.' + config.region + config.endpoint.substring(insertPoint);
+    }
+  },
+
+  validateService: function validateService() {
+    this.optInRegionalEndpoint();
   }
+
 });
diff --git a/scripts/region-checker/whitelist.js b/scripts/region-checker/whitelist.js
index 0a6aee083..8d7636230 100644
--- a/scripts/region-checker/whitelist.js
+++ b/scripts/region-checker/whitelist.js
@@ -2,7 +2,7 @@ var whitelist = {
     '/config.js': [
         24,
         25,
-        179
+        184
     ],
     '/credentials/cognito_identity_credentials.js': [
         78,


Test patch:
diff --git a/test/credentials.spec.js b/test/credentials.spec.js
index b8bb9f9e1..00f038300 100644
--- a/test/credentials.spec.js
+++ b/test/credentials.spec.js
@@ -1383,6 +1383,10 @@
     describe('AWS.TokenFileWebIdentityCredentials', function() {
       var origEnv;
       var fs = require('fs');
+      const defaultCredentials = {
+        AccessKeyId: 'AccessKeyIdDefault',
+        SecretAccessKey: 'SecretAccessKeyDefault'
+      };
       beforeEach(function() {
         origEnv = process.env;
         process.env = {
@@ -1411,7 +1415,16 @@
         expect(credentials.service).not.to.exist;
         credentials.refresh(function() {
           expect(credentials.service).to.exist;
-          done();
+          helpers.spyOn(credentials.service, 'assumeRoleWithWebIdentity').andCallFake(function(params, cb) {
+            return cb(null, {
+              Credentials: defaultCredentials
+            });
+          });
+          credentials.refresh(function() {
+            expect(credentials.accessKeyId).to.equal(defaultCredentials.AccessKeyId);
+            expect(credentials.secretAccessKey).to.equal(defaultCredentials.SecretAccessKey);
+            done();
+          });
         });
       });
 
@@ -1451,12 +1464,15 @@
       it('updates OIDCToken in webIdentityCredentials when new one is returned by token file', function(done) {
         var credentials = new AWS.TokenFileWebIdentityCredentials();
         credentials.refresh(function() {
-          expect(credentials.service.config.params.WebIdentityToken).to.equal('oidcToken');
-          var updatedOidcToken = 'updatedOidcToken';
-          helpers.spyOn(fs, 'readFileSync').andReturn(updatedOidcToken);
+          var assumeRoleWithWebIdentitySpy = helpers.spyOn(credentials.service, 'assumeRoleWithWebIdentity').andCallThrough();
           credentials.refresh(function() {
-            expect(credentials.service.config.params.WebIdentityToken).to.equal(updatedOidcToken);
-            done();
+            expect(assumeRoleWithWebIdentitySpy.calls[0]['arguments'][0].WebIdentityToken).to.equal('oidcToken');
+            var updatedOidcToken = 'updatedOidcToken';
+            helpers.spyOn(fs, 'readFileSync').andReturn(updatedOidcToken);
+            credentials.refresh(function() {
+              expect(assumeRoleWithWebIdentitySpy.calls[1]['arguments'][0].WebIdentityToken).to.equal(updatedOidcToken);
+              done();
+            });
           });
         });
       });
@@ -1474,6 +1490,82 @@
         });
       });
 
+      it('supports chaining in absense of web_identity_token_file and has source_profile', function(done) {
+        delete process.env.AWS_WEB_IDENTITY_TOKEN_FILE;
+        helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn(
+          {
+            'source': {
+              'web_identity_token_file': 'cfgTokenFileSource',
+              'role_arn': 'cfgRoleArnSource'
+            },
+            'default': {
+              'source_profile': 'source',
+              'role_arn': 'cfgRoleArnDefault'
+            }
+          }
+        );
+        var credentials = new AWS.TokenFileWebIdentityCredentials();
+        credentials.refresh(function() {
+          expect(fs.readFileSync.calls[0]['arguments'][0]).to.equal('cfgTokenFileSource');
+          const sourceCredentials = {
+            AccessKeyId: 'AccessKeyIdSource',
+            SecretAccessKey: 'SecretAccessKeySource'
+          };
+          var assumeRoleWithWebIdentitySpy = helpers.spyOn(credentials.service, 'assumeRoleWithWebIdentity').andCallFake(function(params, cb) {
+            return cb(null, {
+              Credentials: sourceCredentials
+            });
+          });
+          var assumeRoleSpy = helpers.spyOn(credentials.service, 'assumeRole').andCallFake(function(params, cb) {
+            expect(credentials.service.config.credentials, sourceCredentials);
+            return cb(null, {
+              Credentials: defaultCredentials
+            });
+          });
+          credentials.refresh(function() {
+            expect(assumeRoleWithWebIdentitySpy.calls[0]['arguments'][0].RoleArn).to.equal('cfgRoleArnSource');
+            expect(assumeRoleSpy.calls[0]['arguments'][0].RoleArn).to.equal('cfgRoleArnDefault');
+            expect(credentials.accessKeyId).to.equal(defaultCredentials.AccessKeyId);
+            expect(credentials.secretAccessKey).to.equal(defaultCredentials.SecretAccessKey);
+            done();
+          });
+        });
+      });
+
+      it('ignores chaining in presence of web_identity_token_file', function(done) {
+        delete process.env.AWS_WEB_IDENTITY_TOKEN_FILE;
+        helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn(
+          {
+            'source': {
+              'web_identity_token_file': 'cfgTokenFileSource',
+              'role_arn': 'cfgRoleArnSource'
+            },
+            'default': {
+              'source_profile': 'source',
+              'web_identity_token_file': 'cfgTokenFileDefault',
+              'role_arn': 'cfgRoleArnDefault'
+            }
+          }
+        );
+        var credentials = new AWS.TokenFileWebIdentityCredentials();
+        credentials.refresh(function() {
+          expect(fs.readFileSync.calls[0]['arguments'][0]).to.equal('cfgTokenFileDefault');
+          var assumeRoleWithWebIdentitySpy = helpers.spyOn(credentials.service, 'assumeRoleWithWebIdentity').andCallFake(function(params, cb) {
+            return cb(null, {
+              Credentials: defaultCredentials
+            });
+          });
+          var assumeRoleSpy = helpers.spyOn(credentials.service, 'assumeRole').andCallThrough();
+          credentials.refresh(function() {
+            expect(assumeRoleWithWebIdentitySpy.calls[0]['arguments'][0].RoleArn).to.equal('cfgRoleArnDefault');
+            expect(assumeRoleSpy.calls.length).to.equal(0);
+            expect(credentials.accessKeyId).to.equal(defaultCredentials.AccessKeyId);
+            expect(credentials.secretAccessKey).to.equal(defaultCredentials.SecretAccessKey);
+            done();
+          });
+        });
+      });
+
       return it('fails if params are not available in both environment variables or shared config', function(done) {
         delete process.env.AWS_WEB_IDENTITY_TOKEN_FILE;
         helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn({});
diff --git a/test/services/sts.spec.js b/test/services/sts.spec.js
index 191e6f9d1..37c09de6e 100644
--- a/test/services/sts.spec.js
+++ b/test/services/sts.spec.js
@@ -13,9 +13,11 @@
       return sts = new AWS.STS();
     });
     describe('credentialsFrom', function() {
+
       it('returns null if no data is provided', function() {
         return expect(sts.credentialsFrom(null)).to.equal(null);
       });
+
       it('creates a TemporaryCredentials object with hydrated data', function() {
         var creds;
         creds = sts.credentialsFrom({
@@ -33,7 +35,8 @@
         expect(creds.expireTime).to.eql(new Date(0));
         return expect(creds.expired).to.equal(false);
       });
-      return it('updates an existing Credentials object with hydrated data', function() {
+
+      it('updates an existing Credentials object with hydrated data', function() {
         var creds, data;
         data = {
           Credentials: {
@@ -53,6 +56,7 @@
         return expect(creds.expired).to.equal(false);
       });
     });
+
     describe('assumeRoleWithWebIdentity', function() {
       var service;
       service = new AWS.STS;
@@ -74,7 +78,8 @@
           return expect(hr.path).to.equal('/');
         });
       });
-      return it('can build a post request on a mounted path (custom endpoint)', function() {
+
+      it('can build a post request on a mounted path (custom endpoint)', function() {
         var params;
         helpers.mockHttpResponse(200, {}, '{}');
         service = new AWS.STS({
@@ -93,7 +98,8 @@
         });
       });
     });
-    return describe('assumeRoleWithSAML', function() {
+
+    describe('assumeRoleWithSAML', function() {
       var service;
       service = new AWS.STS;
       return it('sends an unsigned POST request', function() {
@@ -115,6 +121,167 @@
         });
       });
     });
+
+    describe('regional endpoints', function() {
+      describe('stsRegionalConfig client config', function() {
+        it ('should set the service client stsRegionalConfig config', function() {
+          var values = ['regional', 'RegionaL', 'legacy', 'LegacY'];
+          for (var i = 0; i < values.length; i++) {
+            var sts = new AWS.STS({stsRegionalEndpoints: values[i]});
+            expect(['regional', 'legacy'].indexOf(sts.config.stsRegionalEndpoints) >= 0).to.equal(true);
+          }
+        });
+
+        it('should throw if the config is set to invalid values', function() {
+          var values = ['foo', 'bar', 'region'];
+          var errors = [];
+          for (var i = 0; i < values.length; i++) {
+            try {
+              new AWS.STS({stsRegionalEndpoints: values[i]});
+            } catch (e) {
+              errors.push(e);
+            }
+          }
+          expect(errors.length).to.equal(values.length);
+          for (var i = 0; i < errors.length; i++) {
+            expect(errors[i].code).to.equal('InvalidConfiguration');
+          }
+        });
+      });
+
+      if (AWS.util.isNode()) {
+        describe('AWS_STS_REGIONAL_ENDPOINTS environmental variable', function() {
+          var originalEnv;
+          beforeEach(function() {
+            originalEnv = process.env;
+            process.env = {};
+          });
+          afterEach(function() {
+            process.env = originalEnv;
+          });
+          it('should be used if client config is not set', function() {
+            process.env.AWS_STS_REGIONAL_ENDPOINTS = 'Regional';
+            var sts = new AWS.STS();
+            expect(sts.config.stsRegionalEndpoints).to.equal('regional');
+            process.env.AWS_STS_REGIONAL_ENDPOINTS = 'LegacY';
+            sts = new AWS.STS();
+            expect(sts.config.stsRegionalEndpoints).to.equal('legacy');
+          });
+
+          it('should throw if the config is set to invalid values', function() {
+            var values = ['foo', 'bar', 'region'];
+            var errors = [];
+            for (var i = 0; i < values.length; i++) {
+              try {
+                process.env.AWS_STS_REGIONAL_ENDPOINTS = values[i];
+                new AWS.STS();
+              } catch (e) {
+                errors.push(e);
+              }
+            }
+            expect(errors.length).to.equal(values.length);
+            for (var i = 0; i < errors.length; i++) {
+              expect(errors[i].code).to.equal('InvalidEnvironmentalVariable');
+            }
+          });
+        });
+
+        describe('sts_regional_endpoints config file entry', function() {
+          it('should be used if environmental variable is not set', function() {
+            helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn({
+              default: {
+                sts_regional_endpoints: 'RegionaL'
+              }
+            });
+            var sts = new AWS.STS();
+            expect(sts.config.stsRegionalEndpoints).to.equal('regional');
+            helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn({
+              default: {
+                sts_regional_endpoints: 'LegaCy'
+              }
+            });
+            var sts = new AWS.STS();
+            expect(sts.config.stsRegionalEndpoints).to.equal('legacy');
+          });
+          it('should throw if the config is set to invalid values', function() {
+            var values = ['foo', 'bar', 'region'];
+            var errors = [];
+            for (var i = 0; i < values.length; i++) {
+              helpers.spyOn(AWS.util, 'getProfilesFromSharedConfig').andReturn({
+                default: {
+                  sts_regional_endpoints: values[i]
+                }
+              });
+              try {
+                new AWS.STS();
+              } catch (e) {
+                errors.push(e);
+              }
+            }
+            expect(errors.length).to.equal(values.length);
+            for (var i = 0; i < errors.length; i++) {
+              expect(errors[i].code).to.equal('InvalidConfiguration');
+            }
+          });
+        });
+      }
+      describe('service client stsRegionalConfig config', function() {
+        var originalRegion;
+        var originalEnv;
+        beforeEach(function() {
+          originalRegion = AWS.config.region;
+          AWS.config.region = undefined;
+          //fix CodeBuild test because it comes with AWS_REGION in environment
+          if (AWS.util.isNode()) {
+            originalEnv = process.env;
+            process.env = originalEnv;
+          }
+        });
+        afterEach(function() {
+          AWS.config.region = originalRegion;
+          if (AWS.util.isNode()) {
+            process.env = {};
+          }
+        });
+        it('should use global endpoints for when config is undefined', function() {
+          var regions = ['us-west-2', 'ap-east-1'];
+          for (var i = 0; i < regions.length; i++) {
+            var sts = new AWS.STS({region: regions[i]});
+            expect(sts.config.endpoint).to.contain('sts.amazonaws.com');
+          }
+          var sts = new AWS.STS({region: 'cn-north-1'});
+          expect(sts.config.endpoint).to.contain('sts.cn-north-1.amazonaws.com.cn');
+        });
+        it('should use global endpoints for when config is set to legacy', function() {
+          var regions = ['us-west-2', 'ap-east-1'];
+          for (var i = 0; i < regions.length; i++) {
+            var sts = new AWS.STS({region: regions[i], stsRegionalEndpoints: 'legacy'});
+            expect(sts.config.endpoint).to.contain('sts.amazonaws.com');
+          }
+          var sts = new AWS.STS({region: 'cn-north-1', stsRegionalEndpoints: 'legacy'});
+          expect(sts.config.endpoint).to.contain('sts.cn-north-1.amazonaws.com.cn');
+        });
+        it('should use regional endpoints for when config is set to regional', function() {
+          var regions = ['us-west-2', 'ap-east-1'];
+          for (var i = 0; i < regions.length; i++) {
+            var sts = new AWS.STS({region: regions[i], stsRegionalEndpoints: 'regional'});
+            expect(sts.config.endpoint).to.contain('sts.' + regions[i] + '.amazonaws.com');
+          }
+          var sts = new AWS.STS({region: 'cn-north-1', stsRegionalEndpoints: 'regional'});
+          expect(sts.config.endpoint).to.contain('sts.cn-north-1.amazonaws.com.cn');
+        });
+        it('should ask for region if stsRegionalEndpoints is set', function() {
+          var error;
+          try {
+            new AWS.STS({stsRegionalEndpoints: 'regional'});
+          } catch (e) {
+            error = e;
+          }
+          expect(error.code).to.equal('ConfigError');
+          expect(error.message).to.equal('Missing region in config');
+        });
+      });
+    });
   });
 
 }).call(this);