Skip to content

Commit

Permalink
adds support for sso credentials
Browse files Browse the repository at this point in the history
  • Loading branch information
smoench committed Jul 10, 2023
1 parent 68b9c61 commit e4f4435
Show file tree
Hide file tree
Showing 22 changed files with 743 additions and 0 deletions.
3 changes: 3 additions & 0 deletions couscous.yml
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,9 @@ menu:
ssm:
text: SSM
url: /clients/ssm.html
sso:
text: SSO
url: /clients/sso.html
sts:
text: STS
url: /clients/sts.html
Expand Down
1 change: 1 addition & 0 deletions docs/clients/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ for more information.
| [SNS](./sns.md) | [async-aws/sns](https://packagist.org/packages/async-aws/sns) |
| [SQS](./sqs.md) | [async-aws/sqs](https://packagist.org/packages/async-aws/sqs) |
| [SSM](./ssm.md) | [async-aws/ssm](https://packagist.org/packages/async-aws/ssm) |
| [SSO](./sso.md) | [async-aws/core](https://packagist.org/packages/async-aws/core) |
| [STS](./sts.md) | [async-aws/core](https://packagist.org/packages/async-aws/core) |
| [StepFunctions](./step-functions.md) | [async-aws/step-functions](https://packagist.org/packages/async-aws/step-functions) |
| [TimestreamQuery](./timestream-query.md) | [async-aws/timestream-query](https://packagist.org/packages/async-aws/timestream-query) |
Expand Down
29 changes: 29 additions & 0 deletions docs/clients/sso.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
layout: client
category: clients
name: SSO
package: async-aws/core
fqcn: AsyncAws\Core\Sso\SsoClient
---

## Usage

### Retrieve role credentials

```php
use AsyncAws\Core\Sso\Input\GetRoleCredentialsRequest;
use AsyncAws\Core\Sso\SsoClient;

$client = new StsClient();

$result = $client->getRoleCredentials(new GetRoleCredentialsRequest([
'roleName' => 'YourRoleName',
'accountId' => 'YourAccountId',
'accessToken' => 'YourAccessToken',
]));

echo 'AccessKeyId:' . $result->getRoleCredentials()->getAccessKeyId().PHP_EOL;
echo 'Expiration:' . $result->getRoleCredentials()->getExpiration().PHP_EOL;
echo 'SecretAccessKey:' . $result->getRoleCredentials()->getSecretAccessKey().PHP_EOL;
echo 'SessionToken:' . $result->getRoleCredentials()->getSessionToken();
```
10 changes: 10 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,16 @@
"PutParameter"
]
},
"Sso": {
"source": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/api-2.json",
"documentation": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/docs-2.json",
"pagination": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sso/2019-06-10/paginators-1.json",
"namespace": "AsyncAws\\Core\\Sso",
"api-reference": "https://docs.aws.amazon.com/singlesignon/latest/PortalAPIReference",
"methods": [
"GetRoleCredentials"
]
},
"Sts": {
"source": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sts/2011-06-15/api-2.json",
"documentation": "https://raw.githubusercontent.com/aws/aws-sdk-php/${LATEST}/src/data/sts/2011-06-15/docs-2.json",
Expand Down
1 change: 1 addition & 0 deletions src/Core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Added

- Support for LocationService
- Support for SSO credentials

## 1.19.0

Expand Down
10 changes: 10 additions & 0 deletions src/Core/src/AwsClientFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
use AsyncAws\Core\Credentials\CredentialProvider;
use AsyncAws\Core\Exception\InvalidArgument;
use AsyncAws\Core\Exception\MissingDependency;
use AsyncAws\Core\Sso\SsoClient;
use AsyncAws\Core\Sts\StsClient;
use AsyncAws\DynamoDb\DynamoDbClient;
use AsyncAws\Ecr\EcrClient;
Expand Down Expand Up @@ -519,6 +520,15 @@ public function ssm(): SsmClient
return $this->serviceCache[__METHOD__];
}

public function sso(): SsoClient
{
if (!isset($this->serviceCache[__METHOD__])) {
$this->serviceCache[__METHOD__] = new SsoClient($this->configuration, $this->credentialProvider, $this->httpClient, $this->logger);
}

return $this->serviceCache[__METHOD__];
}

public function sts(): StsClient
{
if (!isset($this->serviceCache[__METHOD__])) {
Expand Down
4 changes: 4 additions & 0 deletions src/Core/src/Credentials/IniFileLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ final class IniFileLoader
public const KEY_ROLE_SESSION_NAME = 'role_session_name';
public const KEY_SOURCE_PROFILE = 'source_profile';
public const KEY_WEB_IDENTITY_TOKEN_FILE = 'web_identity_token_file';
public const KEY_SSO_START_URL = 'sso_start_url';
public const KEY_SSO_REGION = 'sso_region';
public const KEY_SSO_ACCOUNT_ID = 'sso_account_id';
public const KEY_SSO_ROLE_NAME = 'sso_role_name';

/**
* @var LoggerInterface
Expand Down
57 changes: 57 additions & 0 deletions src/Core/src/Credentials/IniFileProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use AsyncAws\Core\Configuration;
use AsyncAws\Core\Exception\RuntimeException;
use AsyncAws\Core\Sso\SsoClient;
use AsyncAws\Core\Sts\StsClient;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
Expand Down Expand Up @@ -92,6 +93,10 @@ private function getCredentialsFromProfile(array $profilesData, string $profile,
return $this->getCredentialsFromRole($profilesData, $profileData, $profile, $circularCollector);
}

if (isset($profileData[IniFileLoader::KEY_SSO_START_URL])) {
return $this->getCredentialsFromLegacySso($profileData, $profile);
}

$this->logger->info('No credentials found for profile "{profile}".', ['profile' => $profile]);

return null;
Expand Down Expand Up @@ -146,4 +151,56 @@ private function getCredentialsFromRole(array $profilesData, array $profileData,
Credentials::adjustExpireDate($credentials->getExpiration(), $this->getDateFromResult($result))
);
}

/**
* @param array<string, string> $profileData
*/
private function getCredentialsFromLegacySso(array $profileData, string $profile): ?Credentials
{
if (!isset(
$profileData[IniFileLoader::KEY_SSO_START_URL],
$profileData[IniFileLoader::KEY_SSO_REGION],
$profileData[IniFileLoader::KEY_SSO_ACCOUNT_ID],
$profileData[IniFileLoader::KEY_SSO_ROLE_NAME]
)) {
$this->logger->warning('Profile "{profile}" does not contains require legacy SSO config.', ['profile' => $profile]);

return null;
}

$ssoCacheFileLoader = new SsoCacheFileLoader($this->logger);
$tokenData = $ssoCacheFileLoader->loadSsoCacheFile($profileData[IniFileLoader::KEY_SSO_START_URL]);

if ([] === $tokenData) {
return null;
}

$ssoClient = new SsoClient(
['region' => $profileData[IniFileLoader::KEY_SSO_REGION]],
null,
$this->httpClient
);
$result = $ssoClient->getRoleCredentials([
'accessToken' => $tokenData[SsoCacheFileLoader::KEY_ACCESS_TOKEN],
'accountId' => $profileData[IniFileLoader::KEY_SSO_ACCOUNT_ID],
'roleName' => $profileData[IniFileLoader::KEY_SSO_ROLE_NAME],
]);

try {
if (null === $credentials = $result->getRoleCredentials()) {
throw new RuntimeException('The RoleCredentials response does not contains credentials');
}
} catch (\Exception $e) {
$this->logger->warning('Failed to get credentials from assumed role in profile "{profile}: {exception}".', ['profile' => $profile, 'exception' => $e]);

return null;
}

return new Credentials(
$credentials->getAccessKeyId(),
$credentials->getSecretAccessKey(),
$credentials->getSessionToken(),
(new \DateTimeImmutable())->setTimestamp($credentials->getExpiration())
);
}
}
79 changes: 79 additions & 0 deletions src/Core/src/Credentials/SsoCacheFileLoader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace AsyncAws\Core\Credentials;

use AsyncAws\Core\EnvVar;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
* Load and parse AWS SSO cache file.
*/
final class SsoCacheFileLoader
{
public const KEY_ACCESS_TOKEN = 'accessToken';
public const KEY_EXPIRES_AT = 'expiresAt';

/**
* @var LoggerInterface
*/
private $logger;

public function __construct(?LoggerInterface $logger = null)
{
$this->logger = $logger ?? new NullLogger();
}

/**
* @return array<string, string>
*/
public function loadSsoCacheFile(string $ssoStartUrl): array
{
$filepath = sprintf('%s/.aws/sso/cache/%s.json', $this->getHomeDir(), sha1($ssoStartUrl));

if (!@is_readable($filepath)) {
$this->logger->warning('The sso cache file {path} is not readable.', ['path' => $filepath]);

return [];
}

$tokenData = json_decode(file_get_contents($filepath), true);
if (!isset($tokenData[self::KEY_ACCESS_TOKEN], $tokenData[self::KEY_EXPIRES_AT])) {
$this->logger->warning('Token file at {path} must contain an accessToken and an expiresAt.', ['path' => $filepath]);

return [];
}

try {
$expiration = (new \DateTimeImmutable($tokenData[self::KEY_EXPIRES_AT]));
} catch (\Exception $e) {
$this->logger->warning('Cached SSO credentials returned an invalid expiresAt value.');

return [];
}

if ($expiration < new \DateTimeImmutable()) {
$this->logger->warning('Cached SSO credentials returned an invalid expiresAt value.');

return [];
}

return $tokenData;
}

private function getHomeDir(): string
{
// On Linux/Unix-like systems, use the HOME environment variable
if (null !== $homeDir = EnvVar::get('HOME')) {
return $homeDir;
}

// Get the HOMEDRIVE and HOMEPATH values for Windows hosts
$homeDrive = EnvVar::get('HOMEDRIVE');
$homePath = EnvVar::get('HOMEPATH');

return ($homeDrive && $homePath) ? $homeDrive . $homePath : '/';
}
}
13 changes: 13 additions & 0 deletions src/Core/src/Sso/Exception/InvalidRequestException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace AsyncAws\Core\Sso\Exception;

use AsyncAws\Core\Exception\Http\ClientException;

/**
* Indicates that a problem occurred with the input to the request. For example, a required parameter might be missing
* or out of range.
*/
final class InvalidRequestException extends ClientException
{
}
12 changes: 12 additions & 0 deletions src/Core/src/Sso/Exception/ResourceNotFoundException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace AsyncAws\Core\Sso\Exception;

use AsyncAws\Core\Exception\Http\ClientException;

/**
* The specified resource doesn't exist.
*/
final class ResourceNotFoundException extends ClientException
{
}
12 changes: 12 additions & 0 deletions src/Core/src/Sso/Exception/TooManyRequestsException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace AsyncAws\Core\Sso\Exception;

use AsyncAws\Core\Exception\Http\ClientException;

/**
* Indicates that the request is being made too frequently and is more than what the server can handle.
*/
final class TooManyRequestsException extends ClientException
{
}
12 changes: 12 additions & 0 deletions src/Core/src/Sso/Exception/UnauthorizedException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

namespace AsyncAws\Core\Sso\Exception;

use AsyncAws\Core\Exception\Http\ClientException;

/**
* Indicates that the request is not authorized. This can happen due to an invalid access token in the request.
*/
final class UnauthorizedException extends ClientException
{
}
Loading

0 comments on commit e4f4435

Please sign in to comment.