Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sso improvements #2628

Merged
merged 20 commits into from
Jan 25, 2023
Merged
7 changes: 7 additions & 0 deletions .changes/nextrelease/enhancement-sso-updates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[
{
"type": "enhancement",
"category": "",
"description": "Enables new SSO login format to be used by the SSO Credential provider"
}
]
161 changes: 104 additions & 57 deletions src/Credentials/CredentialProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ public static function defaultProvider(array $config = [])
|| $config['use_aws_shared_config_files'] !== false
) {
$defaultChain['sso'] = self::sso(
'profile '. $profileName,
$profileName,
self::getHomeDir() . '/.aws/config',
$config
);
Expand Down Expand Up @@ -321,78 +321,98 @@ public static function instanceProfile(array $config = [])
*
* @return callable
*/
public static function sso($ssoProfileName, $filename = null, $config = [])
{
public static function sso($ssoProfileName = 'default',
$filename = null,
$config = []
) {
$filename = $filename ?: (self::getHomeDir() . '/.aws/config');

return function () use ($ssoProfileName, $filename, $config) {
if (!@is_readable($filename)) {
return self::reject("Cannot read credentials from $filename");
}
$profiles = self::loadProfiles($filename);
if (!isset($profiles[$ssoProfileName])) {

if (isset($profiles[$ssoProfileName])) {
$ssoProfile = $profiles[$ssoProfileName];
} elseif (isset($profiles['profile ' . $ssoProfileName])) {
SamRemis marked this conversation as resolved.
Show resolved Hide resolved
$ssoProfileName = 'profile ' . $ssoProfileName;
$ssoProfile = $profiles[$ssoProfileName];
} else {
return self::reject("Profile {$ssoProfileName} does not exist in {$filename}.");
}
$ssoProfile = $profiles[$ssoProfileName];

if (!empty($ssoProfile['sso_session'])) {
return self::reject(
"Profile {$ssoProfileName} contains an sso_session and will rely on"
. " the token provider instead of the legacy sso credential provider."
if (empty($config['ssoOidcClient'])) {
$sessionName = $ssoProfile['sso_session'];
if (empty($profiles['sso-session ' . $sessionName])) {
return self::reject(
"Could not find sso-session {$sessionName} in {$filename}"
);
}
$ssoSession = $profiles['sso-session ' . $ssoProfile['sso_session']];
$ssoOidcClient = new Aws\SSOOIDC\SSOOIDCClient([
'region' => $ssoSession['sso_region'],
'version' => '2019-06-10',
'credentials' => false
]);
} else {
$ssoOidcClient = $config['ssoClient'];
}

$tokenPromise = new Aws\Token\SsoTokenProvider(
$ssoProfileName,
$filename,
$ssoOidcClient
);
}
if (empty($ssoProfile['sso_start_url'])
|| empty($ssoProfile['sso_region'])
|| empty($ssoProfile['sso_account_id'])
|| empty($ssoProfile['sso_role_name'])
) {
return self::reject(
"Profile {$ssoProfileName} in {$filename} must contain the following keys: "
. "sso_start_url, sso_region, sso_account_id, and sso_role_name."
$token = $tokenPromise()->wait();
$ssoCredentials = CredentialProvider::getCredentialsFromSsoService(
$ssoProfile,
$token->getToken(),
$config
);
}
$expiration = $ssoCredentials['expiration'];

$tokenLocation = self::getHomeDir()
. '/.aws/sso/cache/'
. sha1($ssoProfile['sso_start_url'])
. ".json";

if (!@is_readable($tokenLocation)) {
return self::reject("Unable to read token file at $tokenLocation");
}
} else {
if (empty($ssoProfile['sso_start_url'])
|| empty($ssoProfile['sso_region'])
|| empty($ssoProfile['sso_account_id'])
|| empty($ssoProfile['sso_role_name'])
) {
return self::reject(
"Profile {$ssoProfileName} in {$filename} must contain the following keys: "
. "sso_start_url, sso_region, sso_account_id, and sso_role_name."
);
}
$tokenLocation = self::getHomeDir()
. '/.aws/sso/cache/'
. sha1($ssoProfile['sso_start_url'])
. ".json";

$tokenData = json_decode(file_get_contents($tokenLocation), true);
if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) {
return self::reject(
"Token file at {$tokenLocation} must contain an access token and an expiration"
if (!@is_readable($tokenLocation)) {
return self::reject("Unable to read token file at $tokenLocation");
}
$tokenData = json_decode(file_get_contents($tokenLocation), true);
if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) {
return self::reject(
"Token file at {$tokenLocation} must contain an access token and an expiration"
);
}
try {
$expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp();
} catch (\Exception $e) {
return self::reject("Cached SSO credentials returned an invalid expiration");
}
$now = time();
if ($expiration < $now) {
return self::reject("Cached SSO credentials returned expired credentials");
}
$ssoCredentials = CredentialProvider::getCredentialsFromSsoService(
$ssoProfile,
$tokenData['accessToken'],
$config
);
}
try {
$expiration = (new DateTimeResult($tokenData['expiresAt']))->getTimestamp();
} catch (\Exception $e) {
return self::reject("Cached SSO credentials returned an invalid expiration");
}
$now = time();
if ($expiration < $now) {
return self::reject("Cached SSO credentials returned expired credentials");
}

$ssoClient = null;
if (empty($config['ssoClient'])) {
$ssoClient = new Aws\SSO\SSOClient([
'region' => $ssoProfile['sso_region'],
'version' => '2019-06-10',
'credentials' => false
]);
} else {
$ssoClient = $config['ssoClient'];
}
$ssoResponse = $ssoClient->getRoleCredentials([
'accessToken' => $tokenData['accessToken'],
'accountId' => $ssoProfile['sso_account_id'],
'roleName' => $ssoProfile['sso_role_name']
]);

$ssoCredentials = $ssoResponse['roleCredentials'];
return Promise\Create::promiseFor(
new Credentials(
$ssoCredentials['accessKeyId'],
Expand Down Expand Up @@ -905,4 +925,31 @@ public static function shouldUseEcs()
|| !empty(getenv(EcsCredentialProvider::ENV_FULL_URI))
|| !empty($_SERVER[EcsCredentialProvider::ENV_FULL_URI]);
}

/**
* @param array $ssoProfile
* @param string $accessToken
* @param array $config
* @return array|null
*/
private static function getCredentialsFromSsoService($ssoProfile, $accessToken, $config)
{
if (empty($config['ssoClient'])) {
$ssoClient = new Aws\SSO\SSOClient([
'region' => $ssoProfile['sso_region'],
'version' => '2019-06-10',
'credentials' => false
]);
} else {
$ssoClient = $config['ssoClient'];
}
$ssoResponse = $ssoClient->getRoleCredentials([
'accessToken' => $accessToken,
'accountId' => $ssoProfile['sso_account_id'],
'roleName' => $ssoProfile['sso_role_name']
]);

$ssoCredentials = $ssoResponse['roleCredentials'];
return $ssoCredentials;
}
}
2 changes: 1 addition & 1 deletion src/Script/Composer/Composer.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ private static function removeServiceDirs(
$listedServices,
$vendorPath
) {
$unsafeForDeletion = ['Kms', 'S3', 'SSO', 'Sts'];
$unsafeForDeletion = ['Kms', 'S3', 'SSO', 'SSOOIDC', 'Sts'];
if (in_array('DynamoDbStreams', $listedServices)) {
$unsafeForDeletion[] = 'DynamoDb';
}
Expand Down
12 changes: 6 additions & 6 deletions src/Token/ParsesIniTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ trait ParsesIniTrait
private static function loadProfiles($filename)
{
$profileData = \Aws\parse_ini_file($filename, true, INI_SCANNER_RAW);

$configFilename = self::getHomeDir() . '/.aws/config';
$configProfileData = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW);
foreach ($configProfileData as $name => $profile) {
if (is_readable($configFilename)) {
$configProfiles = \Aws\parse_ini_file($configFilename, true, INI_SCANNER_RAW);
$profileData = array_merge($configProfiles, $profileData);
}
foreach ($profileData as $name => $profile) {
// standardize config profile names
$name = str_replace('profile ', '', $name);
if (!isset($profileData[$name])) {
$profileData[$name] = $profile;
}
$profileData[$name] = $profile;
}

return $profileData;
Expand Down
10 changes: 4 additions & 6 deletions src/Token/SsoTokenProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,9 @@ class SsoTokenProvider implements RefreshableTokenProviderInterface
private $ssoOidcClient;

/**
* Constructs a new SSO token object, with the specified AWS
* token
*
* @param string $token Security token to use
* @param int $expires UNIX timestamp for when the token expires
* Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile
* @param string $ssoProfileName The name of the profile that contains the sso_session key
* @param int $filename Name of the config file to sso profile from
*/
public function __construct($ssoProfileName, $filename = null, $ssoOidcClient = null) {
$profileName = getenv(self::ENV_PROFILE) ?: 'default';
Expand All @@ -42,7 +40,7 @@ public function __invoke()
{
return Promise\Coroutine::of(function () {
if (!@is_readable($this->filename)) {
throw new TokenException("Cannot read token from $this->filename");
throw new TokenException("Cannot read profiles from $this->filename");
}
$profiles = self::loadProfiles($this->filename);
if (!isset($profiles[$this->ssoProfileName])) {
Expand Down
Loading