BearerTokenAuthorization.php
11 months ago
ParsesIniTrait.php
11 months ago
RefreshableTokenProviderInterface.php
11 months ago
SsoToken.php
11 months ago
SsoTokenProvider.php
11 months ago
Token.php
11 months ago
TokenAuthorization.php
11 months ago
TokenInterface.php
11 months ago
TokenProvider.php
11 months ago
SsoTokenProvider.php
210 lines
| 1 | <?php |
| 2 | namespace Aws\Token; |
| 3 | |
| 4 | use Aws\Exception\TokenException; |
| 5 | use GuzzleHttp\Promise; |
| 6 | |
| 7 | /** |
| 8 | * Token that comes from the SSO provider |
| 9 | */ |
| 10 | class SsoTokenProvider implements RefreshableTokenProviderInterface |
| 11 | { |
| 12 | use ParsesIniTrait; |
| 13 | |
| 14 | const ENV_PROFILE = 'AWS_PROFILE'; |
| 15 | |
| 16 | private $ssoProfileName; |
| 17 | private $filename; |
| 18 | private $ssoOidcClient; |
| 19 | |
| 20 | /** |
| 21 | * Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile |
| 22 | * @param string $ssoProfileName The name of the profile that contains the sso_session key |
| 23 | * @param int $filename Name of the config file to sso profile from |
| 24 | */ |
| 25 | public function __construct($ssoProfileName, $filename = null, $ssoOidcClient = null) { |
| 26 | $profileName = getenv(self::ENV_PROFILE) ?: 'default'; |
| 27 | $this->ssoProfileName = !empty($ssoProfileName) ? $ssoProfileName : $profileName; |
| 28 | $this->filename = !empty($filename) |
| 29 | ? $filename : |
| 30 | self::getHomeDir() . '/.aws/config'; |
| 31 | $this->ssoOidcClient = $ssoOidcClient; |
| 32 | } |
| 33 | |
| 34 | /* |
| 35 | * Loads cached sso credentials |
| 36 | * |
| 37 | * @return PromiseInterface |
| 38 | */ |
| 39 | public function __invoke() |
| 40 | { |
| 41 | return Promise\Coroutine::of(function () { |
| 42 | if (!@is_readable($this->filename)) { |
| 43 | throw new TokenException("Cannot read profiles from $this->filename"); |
| 44 | } |
| 45 | $profiles = self::loadProfiles($this->filename); |
| 46 | if (!isset($profiles[$this->ssoProfileName])) { |
| 47 | throw new TokenException("Profile {$this->ssoProfileName} does not exist in {$this->filename}."); |
| 48 | } |
| 49 | $ssoProfile = $profiles[$this->ssoProfileName]; |
| 50 | if (empty($ssoProfile['sso_session'])) { |
| 51 | throw new TokenException( |
| 52 | "Profile {$this->ssoProfileName} in {$this->filename} must contain an sso_session." |
| 53 | ); |
| 54 | } |
| 55 | |
| 56 | $sessionProfileName = 'sso-session ' . $ssoProfile['sso_session']; |
| 57 | if (empty($profiles[$sessionProfileName])) { |
| 58 | throw new TokenException( |
| 59 | "Profile {$this->ssoProfileName} does not exist in {$this->filename}" |
| 60 | ); |
| 61 | } |
| 62 | |
| 63 | $sessionProfileData = $profiles[$sessionProfileName]; |
| 64 | if (empty($sessionProfileData['sso_start_url']) |
| 65 | || empty($sessionProfileData['sso_region']) |
| 66 | ) { |
| 67 | throw new TokenException( |
| 68 | "Profile {$this->ssoProfileName} in {$this->filename} must contain the following keys: " |
| 69 | . "sso_start_url and sso_region." |
| 70 | ); |
| 71 | } |
| 72 | |
| 73 | $tokenLocation = self::getTokenLocation($ssoProfile['sso_session']); |
| 74 | if (!@is_readable($tokenLocation)) { |
| 75 | throw new TokenException("Unable to read token file at $tokenLocation"); |
| 76 | } |
| 77 | $tokenData = $this->getTokenData($tokenLocation); |
| 78 | $this->validateTokenData($tokenLocation, $tokenData); |
| 79 | yield new SsoToken( |
| 80 | $tokenData['accessToken'], |
| 81 | $tokenData['expiresAt'], |
| 82 | isset($tokenData['refreshToken']) ? $tokenData['refreshToken'] : null, |
| 83 | isset($tokenData['clientId']) ? $tokenData['clientId'] : null, |
| 84 | isset($tokenData['clientSecret']) ? $tokenData['clientSecret'] : null, |
| 85 | isset($tokenData['registrationExpiresAt']) ? $tokenData['registrationExpiresAt'] : null, |
| 86 | isset($tokenData['region']) ? $tokenData['region'] : null, |
| 87 | isset($tokenData['startUrl']) ? $tokenData['startUrl'] : null |
| 88 | ); |
| 89 | }); |
| 90 | } |
| 91 | |
| 92 | /** |
| 93 | * Refreshes the token |
| 94 | * @return mixed|null |
| 95 | */ |
| 96 | public function refresh() { |
| 97 | try { |
| 98 | //try to reload from disk |
| 99 | $token = $this(); |
| 100 | if ( |
| 101 | $token instanceof SsoToken |
| 102 | && !$token->shouldAttemptRefresh() |
| 103 | ) { |
| 104 | return $token; |
| 105 | } |
| 106 | } finally { |
| 107 | //if reload from disk fails, try refreshing |
| 108 | $tokenLocation = self::getTokenLocation($this->ssoProfileName); |
| 109 | $tokenData = $this->getTokenData($tokenLocation); |
| 110 | if ( |
| 111 | empty($this->ssoOidcClient) |
| 112 | || empty($tokenData['startUrl']) |
| 113 | ) { |
| 114 | throw new TokenException( |
| 115 | "Cannot refresh this token without an 'ssooidcClient' " |
| 116 | . "and a 'start_url'" |
| 117 | ); |
| 118 | } |
| 119 | $response = $this->ssoOidcClient->createToken([ |
| 120 | 'clientId' => $tokenData['clientId'], |
| 121 | 'clientSecret' => $tokenData['clientSecret'], |
| 122 | 'grantType' => 'refresh_token', // REQUIRED |
| 123 | 'refreshToken' => $tokenData['refreshToken'], |
| 124 | ]); |
| 125 | if ($response['@metadata']['statusCode'] == 200) { |
| 126 | $tokenData['accessToken'] = $response['accessToken']; |
| 127 | $tokenData['expiresAt'] = time () + $response['expiresIn']; |
| 128 | $tokenData['refreshToken'] = $response['refreshToken']; |
| 129 | $token = new SsoToken( |
| 130 | $tokenData['accessToken'], |
| 131 | $tokenData['expiresAt'], |
| 132 | $tokenData['refreshToken'], |
| 133 | isset($tokenData['clientId']) ? $tokenData['clientId'] : null, |
| 134 | isset($tokenData['clientSecret']) ? $tokenData['clientSecret'] : null, |
| 135 | isset($tokenData['registrationExpiresAt']) ? $tokenData['registrationExpiresAt'] : null, |
| 136 | isset($tokenData['region']) ? $tokenData['region'] : null, |
| 137 | isset($tokenData['startUrl']) ? $tokenData['startUrl'] : null ); |
| 138 | |
| 139 | $this->writeNewTokenDataToDisk($tokenData, $tokenLocation); |
| 140 | |
| 141 | return $token; |
| 142 | } |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | public function shouldAttemptRefresh() |
| 147 | { |
| 148 | $tokenLocation = self::getTokenLocation($this->ssoProfileName); |
| 149 | $tokenData = $this->getTokenData($tokenLocation); |
| 150 | return strtotime("-10 minutes") >= strtotime($tokenData['expiresAt']); |
| 151 | } |
| 152 | |
| 153 | /** |
| 154 | * @param $sso_session |
| 155 | * @return string |
| 156 | */ |
| 157 | public static function getTokenLocation($sso_session) |
| 158 | { |
| 159 | return self::getHomeDir() |
| 160 | . '/.aws/sso/cache/' |
| 161 | . mb_convert_encoding(sha1($sso_session), "UTF-8") |
| 162 | . ".json"; |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * @param $tokenLocation |
| 167 | * @return array |
| 168 | */ |
| 169 | function getTokenData($tokenLocation) |
| 170 | { |
| 171 | return json_decode(file_get_contents($tokenLocation), true); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * @param $tokenData |
| 176 | * @param $tokenLocation |
| 177 | * @return mixed |
| 178 | */ |
| 179 | private function validateTokenData($tokenLocation, $tokenData) |
| 180 | { |
| 181 | if (empty($tokenData['accessToken']) || empty($tokenData['expiresAt'])) { |
| 182 | throw new TokenException( |
| 183 | "Token file at {$tokenLocation} must contain an access token and an expiration" |
| 184 | ); |
| 185 | } |
| 186 | |
| 187 | $expiration = strtotime($tokenData['expiresAt']); |
| 188 | if ($expiration === false) { |
| 189 | throw new TokenException("Cached SSO token returned an invalid expiration"); |
| 190 | } elseif ($expiration < time()) { |
| 191 | throw new TokenException("Cached SSO token returned an expired token"); |
| 192 | } |
| 193 | return $tokenData; |
| 194 | } |
| 195 | |
| 196 | /** |
| 197 | * @param array $tokenData |
| 198 | * @param string $tokenLocation |
| 199 | * @return void |
| 200 | */ |
| 201 | private function writeNewTokenDataToDisk(array $tokenData, $tokenLocation) |
| 202 | { |
| 203 | $tokenData['expiresAt'] = gmdate( |
| 204 | 'Y-m-d\TH:i:s\Z', |
| 205 | $tokenData['expiresAt'] |
| 206 | ); |
| 207 | file_put_contents($tokenLocation, json_encode(array_filter($tokenData))); |
| 208 | } |
| 209 | } |
| 210 |