Skip to content

Commit 49ac354

Browse files
authored
adds support for SSO credentials (#1519)
adds support for sso credentials
1 parent 9127c0c commit 49ac354

File tree

5 files changed

+169
-0
lines changed

5 files changed

+169
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
- Support for LocationService
88
- Support for hostPrefix in requests
99
- AWS api-change: API updates for the AWS Security Token Service
10+
- Support for SSO credentials
1011

1112
## 1.19.0
1213

src/AwsClientFactory.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
use AsyncAws\Sns\SnsClient;
4545
use AsyncAws\Sqs\SqsClient;
4646
use AsyncAws\Ssm\SsmClient;
47+
use AsyncAws\Sso\SsoClient;
4748
use AsyncAws\StepFunctions\StepFunctionsClient;
4849
use AsyncAws\TimestreamQuery\TimestreamQueryClient;
4950
use AsyncAws\TimestreamWrite\TimestreamWriteClient;
@@ -519,6 +520,15 @@ public function ssm(): SsmClient
519520
return $this->serviceCache[__METHOD__];
520521
}
521522

523+
public function sso(): SsoClient
524+
{
525+
if (!isset($this->serviceCache[__METHOD__])) {
526+
$this->serviceCache[__METHOD__] = new SsoClient($this->configuration, $this->credentialProvider, $this->httpClient, $this->logger);
527+
}
528+
529+
return $this->serviceCache[__METHOD__];
530+
}
531+
522532
public function sts(): StsClient
523533
{
524534
if (!isset($this->serviceCache[__METHOD__])) {

src/Credentials/IniFileLoader.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ final class IniFileLoader
2323
public const KEY_ROLE_SESSION_NAME = 'role_session_name';
2424
public const KEY_SOURCE_PROFILE = 'source_profile';
2525
public const KEY_WEB_IDENTITY_TOKEN_FILE = 'web_identity_token_file';
26+
public const KEY_SSO_START_URL = 'sso_start_url';
27+
public const KEY_SSO_REGION = 'sso_region';
28+
public const KEY_SSO_ACCOUNT_ID = 'sso_account_id';
29+
public const KEY_SSO_ROLE_NAME = 'sso_role_name';
2630

2731
/**
2832
* @var LoggerInterface

src/Credentials/IniFileProvider.php

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
use AsyncAws\Core\Configuration;
88
use AsyncAws\Core\Exception\RuntimeException;
99
use AsyncAws\Core\Sts\StsClient;
10+
use AsyncAws\Sso\SsoClient;
1011
use Psr\Log\LoggerInterface;
1112
use Psr\Log\NullLogger;
1213
use Symfony\Contracts\HttpClient\HttpClientInterface;
@@ -92,6 +93,16 @@ private function getCredentialsFromProfile(array $profilesData, string $profile,
9293
return $this->getCredentialsFromRole($profilesData, $profileData, $profile, $circularCollector);
9394
}
9495

96+
if (isset($profileData[IniFileLoader::KEY_SSO_START_URL])) {
97+
if (class_exists(SsoClient::class)) {
98+
return $this->getCredentialsFromLegacySso($profileData, $profile);
99+
}
100+
101+
$this->logger->warning('The profile "{profile}" contains SSO (legacy) config but the "async-aws/sso" package is not installed. Try running "composer require async-aws/sso".', ['profile' => $profile]);
102+
103+
return null;
104+
}
105+
95106
$this->logger->info('No credentials found for profile "{profile}".', ['profile' => $profile]);
96107

97108
return null;
@@ -146,4 +157,68 @@ private function getCredentialsFromRole(array $profilesData, array $profileData,
146157
Credentials::adjustExpireDate($credentials->getExpiration(), $this->getDateFromResult($result))
147158
);
148159
}
160+
161+
/**
162+
* @param array<string, string> $profileData
163+
*/
164+
private function getCredentialsFromLegacySso(array $profileData, string $profile): ?Credentials
165+
{
166+
if (!isset(
167+
$profileData[IniFileLoader::KEY_SSO_START_URL],
168+
$profileData[IniFileLoader::KEY_SSO_REGION],
169+
$profileData[IniFileLoader::KEY_SSO_ACCOUNT_ID],
170+
$profileData[IniFileLoader::KEY_SSO_ROLE_NAME]
171+
)) {
172+
$this->logger->warning('Profile "{profile}" does not contains required legacy SSO config.', ['profile' => $profile]);
173+
174+
return null;
175+
}
176+
177+
$ssoCacheFileLoader = new SsoCacheFileLoader($this->logger);
178+
$tokenData = $ssoCacheFileLoader->loadSsoCacheFile($profileData[IniFileLoader::KEY_SSO_START_URL]);
179+
180+
if ([] === $tokenData) {
181+
return null;
182+
}
183+
184+
$ssoClient = new SsoClient(
185+
['region' => $profileData[IniFileLoader::KEY_SSO_REGION]],
186+
new NullProvider(), // no credentials required as we provide an access token via the role credentials request
187+
$this->httpClient
188+
);
189+
$result = $ssoClient->getRoleCredentials([
190+
'accessToken' => $tokenData[SsoCacheFileLoader::KEY_ACCESS_TOKEN],
191+
'accountId' => $profileData[IniFileLoader::KEY_SSO_ACCOUNT_ID],
192+
'roleName' => $profileData[IniFileLoader::KEY_SSO_ROLE_NAME],
193+
]);
194+
195+
try {
196+
if (null === $credentials = $result->getRoleCredentials()) {
197+
throw new RuntimeException('The RoleCredentials response does not contains credentials');
198+
}
199+
if (null === $accessKeyId = $credentials->getAccessKeyId()) {
200+
throw new RuntimeException('The RoleCredentials response does not contain an accessKeyId');
201+
}
202+
if (null === $secretAccessKey = $credentials->getSecretAccessKey()) {
203+
throw new RuntimeException('The RoleCredentials response does not contain a secretAccessKey');
204+
}
205+
if (null === $sessionToken = $credentials->getSessionToken()) {
206+
throw new RuntimeException('The RoleCredentials response does not contain a sessionToken');
207+
}
208+
if (null === $expiration = $credentials->getExpiration()) {
209+
throw new RuntimeException('The RoleCredentials response does not contain an expiration');
210+
}
211+
} catch (\Exception $e) {
212+
$this->logger->warning('Failed to get credentials from role credentials in profile "{profile}: {exception}".', ['profile' => $profile, 'exception' => $e]);
213+
214+
return null;
215+
}
216+
217+
return new Credentials(
218+
$accessKeyId,
219+
$secretAccessKey,
220+
$sessionToken,
221+
(new \DateTimeImmutable())->setTimestamp($expiration)
222+
);
223+
}
149224
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace AsyncAws\Core\Credentials;
6+
7+
use AsyncAws\Core\EnvVar;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\NullLogger;
10+
11+
/**
12+
* Load and parse AWS SSO cache file.
13+
*/
14+
final class SsoCacheFileLoader
15+
{
16+
public const KEY_ACCESS_TOKEN = 'accessToken';
17+
public const KEY_EXPIRES_AT = 'expiresAt';
18+
19+
/**
20+
* @var LoggerInterface
21+
*/
22+
private $logger;
23+
24+
public function __construct(?LoggerInterface $logger = null)
25+
{
26+
$this->logger = $logger ?? new NullLogger();
27+
}
28+
29+
/**
30+
* @return array<string, string>
31+
*/
32+
public function loadSsoCacheFile(string $ssoStartUrl): array
33+
{
34+
$filepath = sprintf('%s/.aws/sso/cache/%s.json', $this->getHomeDir(), sha1($ssoStartUrl));
35+
36+
if (false === ($contents = @file_get_contents($filepath))) {
37+
$this->logger->warning('The sso cache file {path} is not readable.', ['path' => $filepath]);
38+
39+
return [];
40+
}
41+
42+
$tokenData = json_decode($contents, true);
43+
if (!isset($tokenData[self::KEY_ACCESS_TOKEN], $tokenData[self::KEY_EXPIRES_AT])) {
44+
$this->logger->warning('Token file at {path} must contain an accessToken and an expiresAt.', ['path' => $filepath]);
45+
46+
return [];
47+
}
48+
49+
try {
50+
$expiration = (new \DateTimeImmutable($tokenData[self::KEY_EXPIRES_AT]));
51+
} catch (\Exception $e) {
52+
$this->logger->warning('Cached SSO credentials returned an invalid expiresAt value.');
53+
54+
return [];
55+
}
56+
57+
if ($expiration < new \DateTimeImmutable()) {
58+
$this->logger->warning('Cached SSO credentials returned an invalid expiresAt value.');
59+
60+
return [];
61+
}
62+
63+
return $tokenData;
64+
}
65+
66+
private function getHomeDir(): string
67+
{
68+
// On Linux/Unix-like systems, use the HOME environment variable
69+
if (null !== $homeDir = EnvVar::get('HOME')) {
70+
return $homeDir;
71+
}
72+
73+
// Get the HOMEDRIVE and HOMEPATH values for Windows hosts
74+
$homeDrive = EnvVar::get('HOMEDRIVE');
75+
$homePath = EnvVar::get('HOMEPATH');
76+
77+
return ($homeDrive && $homePath) ? $homeDrive . $homePath : '/';
78+
}
79+
}

0 commit comments

Comments
 (0)