Skip to content

Commit d055881

Browse files
authored
bugfix: sts-credential-providers (#3203)
1 parent 8b0a9fe commit d055881

File tree

7 files changed

+1467
-1209
lines changed

7 files changed

+1467
-1209
lines changed

CHANGELOG.md

Lines changed: 1196 additions & 1192 deletions
Large diffs are not rendered by default.

src/Credentials/AssumeRoleWithWebIdentityCredentialProvider.php

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class AssumeRoleWithWebIdentityCredentialProvider
3636

3737
/** @var integer */
3838
private $tokenFileReadAttempts;
39+
3940
/** @var string */
4041
private $source;
4142

@@ -72,15 +73,15 @@ public function __construct(array $config = [])
7273
$this->tokenFileReadAttempts = 0;
7374
$this->session = $config['SessionName']
7475
?? 'aws-sdk-php-' . round(microtime(true) * 1000);
75-
$region = $config['region'] ?? 'us-east-1';
76+
7677
if (isset($config['client'])) {
7778
$this->client = $config['client'];
7879
} else {
79-
$this->client = new StsClient([
80-
'credentials' => false,
81-
'region' => $region,
82-
'version' => 'latest'
83-
]);
80+
$region = $config['region']
81+
?? getEnv(CredentialProvider::ENV_REGION)
82+
?: null;
83+
84+
$this->client = $this->createDefaultStsClient($region);
8485
}
8586

8687
$this->source = $config['source']
@@ -167,4 +168,34 @@ public function __invoke()
167168
);
168169
});
169170
}
171+
172+
/**
173+
* @param string|null $region
174+
*
175+
* @return StsClient
176+
*/
177+
private function createDefaultStsClient(
178+
?string $region
179+
): StsClient
180+
{
181+
if (empty($region)) {
182+
$region = CredentialProvider::FALLBACK_REGION;
183+
trigger_error(
184+
'NOTICE: STS client created without explicit `region` configuration.' . PHP_EOL
185+
. "Defaulting to {$region}. This fallback behavior may be removed." . PHP_EOL
186+
. 'To avoid potential disruptions, configure a region using one of the following methods:' . PHP_EOL
187+
. '(1) Pass `region` in the `$config` array when calling the provider,' . PHP_EOL
188+
. '(2) Set the `AWS_REGION` environment variable.' . PHP_EOL
189+
. 'OR provide an STS client in the `$config` array when creating the provider as `client`.' . PHP_EOL
190+
. 'See: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/assume-role-with-web-identity-provider.html'
191+
. PHP_EOL,
192+
E_USER_NOTICE
193+
);
194+
}
195+
196+
return new StsClient([
197+
'credentials' => false,
198+
'region' => $region
199+
]);
200+
}
170201
}

src/Credentials/CredentialProvider.php

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,8 @@ class CredentialProvider
5252
const ENV_SESSION = 'AWS_SESSION_TOKEN';
5353
const ENV_TOKEN_FILE = 'AWS_WEB_IDENTITY_TOKEN_FILE';
5454
const ENV_SHARED_CREDENTIALS_FILE = 'AWS_SHARED_CREDENTIALS_FILE';
55+
public const ENV_REGION = 'AWS_REGION';
56+
public const FALLBACK_REGION = 'us-east-1';
5557
public const REFRESH_WINDOW = 60;
5658

5759
/**
@@ -102,7 +104,7 @@ public static function defaultProvider(array $config = [])
102104
$config
103105
);
104106
$defaultChain['process_credentials'] = self::process();
105-
$defaultChain['ini'] = self::ini();
107+
$defaultChain['ini'] = self::ini(null, null, $config);
106108
$defaultChain['process_config'] = self::process(
107109
'profile ' . $profileName,
108110
self::getHomeDir() . '/.aws/config'
@@ -708,9 +710,6 @@ private static function loadRoleProfile(
708710
}
709711

710712
if (empty($stsClient)) {
711-
$sourceRegion = isset($profiles[$sourceProfileName]['region'])
712-
? $profiles[$sourceProfileName]['region']
713-
: 'us-east-1';
714713
$config['preferStaticCredentials'] = true;
715714
$sourceCredentials = null;
716715
if (!empty($roleProfile['source_profile'])){
@@ -723,11 +722,13 @@ private static function loadRoleProfile(
723722
$filename
724723
);
725724
}
726-
$stsClient = new StsClient([
727-
'credentials' => $sourceCredentials,
728-
'region' => $sourceRegion,
729-
'version' => '2011-06-15',
730-
]);
725+
726+
$region = $profiles[$sourceProfileName]['region']
727+
?? $config['region']
728+
?? getEnv(self::ENV_REGION)
729+
?: null;
730+
731+
$stsClient = self::createDefaultStsClient($sourceCredentials, $region);
731732
}
732733

733734
$result = $stsClient->assumeRole([
@@ -1026,5 +1027,37 @@ private static function getCredentialsFromSsoService($ssoProfile, $clientRegion,
10261027
$ssoCredentials = $ssoResponse['roleCredentials'];
10271028
return $ssoCredentials;
10281029
}
1030+
1031+
/**
1032+
* @param CredentialsInterface $credentials
1033+
* @param string|null $region
1034+
*
1035+
* @return StsClient
1036+
*/
1037+
private static function createDefaultStsClient(
1038+
CredentialsInterface $credentials,
1039+
?string $region
1040+
): StsClient
1041+
{
1042+
if (empty($region)) {
1043+
$region = self::FALLBACK_REGION;
1044+
trigger_error(
1045+
'NOTICE: STS client created without explicit `region` configuration.' . PHP_EOL
1046+
. "Defaulting to `{$region}`. This fallback behavior may be removed." . PHP_EOL
1047+
. 'To avoid potential disruptions, configure a `region` using one of the following methods:' . PHP_EOL
1048+
. '(1) Add `region` to your source profile in ~/.aws/credentials,' . PHP_EOL
1049+
. '(2) Pass `region` in the `$config` array when calling the provider,' . PHP_EOL
1050+
. '(3) Set the `AWS_REGION` environment variable.' . PHP_EOL
1051+
. 'See: https://docs.aws.amazon.com/sdk-for-php/v3/developer-guide/guide_credentials_assume_role.html#assume-role-with-profile'
1052+
. PHP_EOL,
1053+
E_USER_NOTICE
1054+
);
1055+
}
1056+
1057+
return new StsClient([
1058+
'credentials' => $credentials,
1059+
'region' => $region
1060+
]);
1061+
}
10291062
}
10301063

src/Sdk.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -833,7 +833,7 @@
833833
*/
834834
class Sdk
835835
{
836-
const VERSION = '3.357.3';
836+
const VERSION = '3.358.0';
837837

838838
/** @var array Arguments for creating clients */
839839
private $args;

tests/Credentials/AssumeRoleWithWebIdentityCredentialProviderTest.php

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Aws\Credentials\AssumeRoleWithWebIdentityCredentialProvider;
77
use Aws\Credentials\Credentials;
88
use Aws\Exception\AwsException;
9+
use Aws\Middleware;
910
use Aws\Result;
1011
use Aws\Sts\StsClient;
1112
use Aws\Sts\Exception\StsException;
@@ -153,6 +154,7 @@ public function testThrowsExceptionWhenReadingTokenFileFails()
153154
$this->expectException(\Aws\Exception\CredentialsException::class);
154155
$args['RoleArn'] = self::SAMPLE_ROLE_ARN;
155156
$args['WebIdentityTokenFile'] = '/foo';
157+
$args['region'] = 'us-east-1';
156158
$provider = new AssumeRoleWithWebIdentityCredentialProvider($args);
157159
$provider()->wait();
158160
}
@@ -163,6 +165,7 @@ public function testThrowsExceptionWhenEmptyTokenFile()
163165
$tokenPath = $dir . '/emptyTokenFile';
164166
$args['WebIdentityTokenFile'] = $tokenPath;
165167
$args['RoleArn'] = self::SAMPLE_ROLE_ARN;
168+
$args['region'] = 'us-east-1';
166169
file_put_contents($tokenPath, '');
167170

168171
try {
@@ -396,4 +399,167 @@ public function testCanDisableInvalidIdentityTokenRetries()
396399
unlink($tokenPath);
397400
}
398401
}
402+
403+
/**
404+
* Tests region precedence: config > env var > fallback
405+
* @dataProvider regionPrecedenceProvider
406+
*/
407+
public function testRegionPrecedence(
408+
?string $configRegion,
409+
?string $envRegion,
410+
string $expectedRegion,
411+
bool $expectNotice
412+
): void
413+
{
414+
$originalRegion = getenv('AWS_REGION');
415+
416+
if ($envRegion !== null) {
417+
putenv("AWS_REGION={$envRegion}");
418+
} else {
419+
putenv("AWS_REGION");
420+
}
421+
422+
$tokenFile = tempnam(sys_get_temp_dir(), 'token');
423+
file_put_contents($tokenFile, 'test-token-content');
424+
425+
try {
426+
$config = [
427+
'RoleArn' => self::SAMPLE_ROLE_ARN,
428+
'WebIdentityTokenFile' => $tokenFile,
429+
];
430+
431+
if ($configRegion !== null) {
432+
$config['region'] = $configRegion;
433+
}
434+
435+
if ($expectNotice) {
436+
$this->expectNotice();
437+
$this->expectNoticeMessage(
438+
'NOTICE: STS client created without explicit `region` configuration.'
439+
);
440+
}
441+
442+
$provider = new AssumeRoleWithWebIdentityCredentialProvider($config);
443+
444+
// Check region
445+
$reflection = new \ReflectionClass($provider);
446+
$clientProperty = $reflection->getProperty('client');
447+
$stsClient = $clientProperty->getValue($provider);
448+
449+
$this->assertEquals($expectedRegion, $stsClient->getRegion());
450+
} finally {
451+
// Cleanup
452+
unlink($tokenFile);
453+
454+
// Restore original AWS_REGION value
455+
if ($originalRegion !== false) {
456+
putenv("AWS_REGION={$originalRegion}");
457+
} else {
458+
putenv("AWS_REGION");
459+
}
460+
}
461+
}
462+
463+
public function regionPrecedenceProvider(): array
464+
{
465+
return [
466+
'config overrides env' => ['us-west-2', 'eu-west-1', 'us-west-2', false],
467+
'env used when no config' => [null, 'eu-west-1', 'eu-west-1', false],
468+
'fallback when neither' => [null, null, 'us-east-1', true],
469+
'empty string uses fallback' => ['', null, 'us-east-1', true],
470+
];
471+
}
472+
473+
/**
474+
* Tests that correct endpoints are called
475+
* @dataProvider endpointProvider
476+
*/
477+
public function testEndpointSelection(
478+
string $region,
479+
string $expectedEndpoint
480+
): void
481+
{
482+
$tokenFile = tempnam(sys_get_temp_dir(), 'token');
483+
file_put_contents($tokenFile, 'test-token-content');
484+
485+
$config = [
486+
'RoleArn' => self::SAMPLE_ROLE_ARN,
487+
'WebIdentityTokenFile' => $tokenFile,
488+
'region' => $region
489+
];
490+
491+
$provider = new AssumeRoleWithWebIdentityCredentialProvider($config);
492+
493+
// Get the client via reflection
494+
$reflection = new \ReflectionClass($provider);
495+
$clientProperty = $reflection->getProperty('client');
496+
$stsClient = $clientProperty->getValue($provider);
497+
498+
// Set up middleware to capture the endpoint
499+
$capturedEndpoint = null;
500+
$stsClient->getHandlerList()->appendBuild(
501+
Middleware::tap(
502+
function ($cmd, $req) use (&$capturedEndpoint) {
503+
$capturedEndpoint = (string) $req->getUri();
504+
}
505+
)
506+
);
507+
508+
// Mock the STS response
509+
$stsClient->getHandlerList()->setHandler(
510+
function ($c, $r) {
511+
$result = [
512+
'Credentials' => [
513+
'AccessKeyId' => 'foo',
514+
'SecretAccessKey' => 'bar',
515+
'SessionToken' => 'baz',
516+
'Expiration' => DateTimeResult::fromEpoch(time() + 10)
517+
],
518+
'AssumedRoleUser' => [
519+
'AssumedRoleId' => 'ARXXXXXXXXXXXXXXXXXXX:test_session',
520+
'Arn' => self::SAMPLE_ROLE_ARN . "/test_session"
521+
]
522+
];
523+
return Promise\Create::promiseFor(new Result($result));
524+
}
525+
);
526+
527+
try {
528+
$provider()->wait();
529+
530+
$this->assertEquals(
531+
$expectedEndpoint,
532+
$capturedEndpoint,
533+
"Failed asserting endpoint for region: {$region}"
534+
);
535+
} finally {
536+
unlink($tokenFile);
537+
}
538+
}
539+
540+
public function endpointProvider(): array
541+
{
542+
return [
543+
'us-east-1' => [
544+
'region' => 'us-east-1',
545+
'expectedEndpoint' => 'https://sts.us-east-1.amazonaws.com/'
546+
],
547+
'us-west-2' => [
548+
'region' => 'us-west-2',
549+
'expectedEndpoint' => 'https://sts.us-west-2.amazonaws.com/'
550+
],
551+
'eu-west-1' => [
552+
'region' => 'eu-west-1',
553+
'expectedEndpoint' => 'https://sts.eu-west-1.amazonaws.com/'
554+
],
555+
'ap-southeast-1' => [
556+
'region' => 'ap-southeast-1',
557+
'expectedEndpoint' => 'https://sts.ap-southeast-1.amazonaws.com/'
558+
],
559+
'sa-east-1' => [
560+
'region' => 'sa-east-1',
561+
'expectedEndpoint' => 'https://sts.sa-east-1.amazonaws.com/'
562+
]
563+
];
564+
}
399565
}

tests/Credentials/CredentialProviderTest.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use Aws\LruArrayCache;
1313
use Aws\Result;
1414
use Aws\SSO\SSOClient;
15+
use Aws\Sts\Exception\StsException;
1516
use Aws\Sts\StsClient;
1617
use Aws\Token\SsoTokenProvider;
1718
use Aws\Test\UsesServiceTrait;
@@ -688,6 +689,27 @@ public function testCreatesFromRoleArn(): void
688689
$this->assertFalse($creds->isExpired());
689690
}
690691

692+
public function testCreatesFromRoleArnEmitsNoticeOnFallbackRegion(): void
693+
{
694+
$this->expectNotice();
695+
$this->expectNoticeMessage(
696+
'NOTICE: STS client created without explicit `region` configuration'
697+
);
698+
699+
$awsDir = $this->createAwsHome();
700+
$ini = <<<EOT
701+
[default]
702+
aws_access_key_id = foo
703+
aws_secret_access_key = defaultSecret
704+
[assume]
705+
role_arn = arn:aws:iam::012345678910:role/role_name
706+
source_profile = default
707+
role_session_name = foobar
708+
EOT;
709+
file_put_contents($awsDir . '/credentials', $ini);
710+
call_user_func(CredentialProvider::ini('assume', null))->wait();
711+
}
712+
691713
public function testCreatesFromRoleArnCatchesCircular(): void
692714
{
693715
$this->expectExceptionMessage("Circular source_profile reference found.");

0 commit comments

Comments
 (0)