PluginProbe
Media Cloud Sync / 1.3.11
Media Cloud Sync v1.3.11
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / sdk / s3 / Aws / Token / SsoTokenProvider.php

SsoTokenProvider.php in Media Cloud Sync 1.3.11, at includes/sdk/s3/Aws/Token/SsoTokenProvider.php

226 lines 9.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\Aws\Token;
4
5 use Dudlewebs\WPMCS\s3\Aws\Exception\TokenException;
6 use Dudlewebs\WPMCS\s3\Aws\SSOOIDC\SSOOIDCClient;
7 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise;
8 /**
9 * Token that comes from the SSO provider
10 */
11 class SsoTokenProvider implements RefreshableTokenProviderInterface
12 {
13 use ParsesIniTrait;
14 const ENV_PROFILE = 'AWS_PROFILE';
15 const REFRESH_WINDOW_IN_SECS = 300;
16 const REFRESH_ATTEMPT_WINDOW_IN_SECS = 30;
17 /** @var string $profileName */
18 private $profileName;
19 /** @var string $configFilePath */
20 private $configFilePath;
21 /** @var SSOOIDCClient $ssoOidcClient */
22 private $ssoOidcClient;
23 /** @var string $ssoSessionName */
24 private $ssoSessionName;
25 /**
26 * Constructs a new SsoTokenProvider object, which will fetch a token from an authenticated SSO profile
27 * @param string $profileName The name of the profile that contains the sso_session key
28 * @param string|null $configFilePath Name of the config file to sso profile from
29 * @param SSOOIDCClient|null $ssoOidcClient The sso client for generating a new token
30 */
31 public function __construct($profileName, $configFilePath = null, ?SSOOIDCClient $ssoOidcClient = null)
32 {
33 $this->profileName = $this->resolveProfileName($profileName);
34 $this->configFilePath = $this->resolveConfigFile($configFilePath);
35 $this->ssoOidcClient = $ssoOidcClient;
36 }
37 /**
38 * This method resolves the profile name to be used. The
39 * profile provided as instantiation argument takes precedence,
40 * followed by AWS_PROFILE env variable, otherwise `default` is
41 * used.
42 *
43 * @param string|null $argProfileName The profile provided as argument.
44 *
45 * @return string
46 */
47 private function resolveProfileName($argProfileName) : string
48 {
49 if (empty($argProfileName)) {
50 return \getenv(self::ENV_PROFILE) ?: 'default';
51 } else {
52 return $argProfileName;
53 }
54 }
55 /**
56 * This method resolves the config file from where the profiles
57 * are going to be loaded from. If $argFileName is not empty then,
58 * it takes precedence over the default config file location.
59 *
60 * @param string|null $argConfigFilePath The config path provided as argument.
61 *
62 * @return string
63 */
64 private function resolveConfigFile($argConfigFilePath) : string
65 {
66 if (empty($argConfigFilePath)) {
67 return self::getHomeDir() . '/.aws/config';
68 } else {
69 return $argConfigFilePath;
70 }
71 }
72 /**
73 * Loads cached sso credentials.
74 *
75 * @return Promise\PromiseInterface
76 */
77 public function __invoke()
78 {
79 return Promise\Coroutine::of(function () {
80 if (empty($this->configFilePath) || !\is_readable($this->configFilePath)) {
81 throw new TokenException("Cannot read profiles from {$this->configFilePath}");
82 }
83 $profiles = self::loadProfiles($this->configFilePath);
84 if (!isset($profiles[$this->profileName])) {
85 throw new TokenException("Profile `{$this->profileName}` does not exist in {$this->configFilePath}.");
86 }
87 $profile = $profiles[$this->profileName];
88 if (empty($profile['sso_session'])) {
89 throw new TokenException("Profile `{$this->profileName}` in {$this->configFilePath} must contain an sso_session.");
90 }
91 $ssoSessionName = $profile['sso_session'];
92 $this->ssoSessionName = $ssoSessionName;
93 $profileSsoSession = 'sso-session ' . $ssoSessionName;
94 if (empty($profiles[$profileSsoSession])) {
95 throw new TokenException("Sso session `{$ssoSessionName}` does not exist in {$this->configFilePath}");
96 }
97 $sessionProfileData = $profiles[$profileSsoSession];
98 foreach (['sso_start_url', 'sso_region'] as $requiredProp) {
99 if (empty($sessionProfileData[$requiredProp])) {
100 throw new TokenException("Sso session `{$ssoSessionName}` in {$this->configFilePath} is missing the required property `{$requiredProp}`");
101 }
102 }
103 $tokenData = $this->refresh();
104 $tokenLocation = self::getTokenLocation($ssoSessionName);
105 $this->validateTokenData($tokenLocation, $tokenData);
106 $ssoToken = SsoToken::fromTokenData($tokenData);
107 // To make sure the token is not expired
108 if ($ssoToken->isExpired()) {
109 throw new TokenException("Cached SSO token returned an expired token.");
110 }
111 (yield $ssoToken);
112 });
113 }
114 /**
115 * This method attempt to refresh when possible.
116 * If a refresh is not possible then it just returns
117 * the current token data as it is.
118 *
119 * @return array
120 * @throws TokenException
121 */
122 public function refresh() : array
123 {
124 $tokenLocation = self::getTokenLocation($this->ssoSessionName);
125 $tokenData = $this->getTokenData($tokenLocation);
126 if (!$this->shouldAttemptRefresh()) {
127 return $tokenData;
128 }
129 if (null === $this->ssoOidcClient) {
130 throw new TokenException("Cannot refresh this token without an 'ssooidcClient' ");
131 }
132 foreach (['clientId', 'clientSecret', 'refreshToken'] as $requiredProp) {
133 if (empty($tokenData[$requiredProp])) {
134 throw new TokenException("Cannot refresh this token without `{$requiredProp}` being set");
135 }
136 }
137 $response = $this->ssoOidcClient->createToken([
138 'clientId' => $tokenData['clientId'],
139 'clientSecret' => $tokenData['clientSecret'],
140 'grantType' => 'refresh_token',
141 // REQUIRED
142 'refreshToken' => $tokenData['refreshToken'],
143 ]);
144 if ($response['@metadata']['statusCode'] !== 200) {
145 throw new TokenException('Unable to create a new sso token');
146 }
147 $tokenData['accessToken'] = $response['accessToken'];
148 $tokenData['expiresAt'] = \time() + $response['expiresIn'];
149 $tokenData['refreshToken'] = $response['refreshToken'];
150 return $this->writeNewTokenDataToDisk($tokenData, $tokenLocation);
151 }
152 /**
153 * This method checks for whether a token refresh should happen.
154 * It will return true just if more than 30 seconds has happened
155 * since last refresh, and if the expiration is within a 5-minutes
156 * window from the current time.
157 *
158 * @return bool
159 */
160 public function shouldAttemptRefresh() : bool
161 {
162 $tokenLocation = self::getTokenLocation($this->ssoSessionName);
163 $tokenData = $this->getTokenData($tokenLocation);
164 if (empty($tokenData['expiresAt'])) {
165 throw new TokenException("Token file at {$tokenLocation} must contain an expiration date");
166 }
167 $tokenExpiresAt = \strtotime($tokenData['expiresAt']);
168 $lastRefreshAt = \filemtime($tokenLocation);
169 $now = \time();
170 // If last refresh happened after 30 seconds
171 // and if the token expiration is in the 5 minutes window
172 return $now - $lastRefreshAt > self::REFRESH_ATTEMPT_WINDOW_IN_SECS && $tokenExpiresAt - $now < self::REFRESH_WINDOW_IN_SECS;
173 }
174 /**
175 * @param $sso_session
176 * @return string
177 */
178 public static function getTokenLocation($sso_session) : string
179 {
180 return self::getHomeDir() . '/.aws/sso/cache/' . \mb_convert_encoding(\sha1($sso_session), "UTF-8") . ".json";
181 }
182 /**
183 * @param $tokenLocation
184 * @return array
185 */
186 function getTokenData($tokenLocation) : array
187 {
188 if (empty($tokenLocation) || !\is_readable($tokenLocation)) {
189 throw new TokenException("Unable to read token file at {$tokenLocation}");
190 }
191 return \json_decode(\file_get_contents($tokenLocation), \true);
192 }
193 /**
194 * @param $tokenData
195 * @param $tokenLocation
196 * @return mixed
197 */
198 private function validateTokenData($tokenLocation, $tokenData)
199 {
200 foreach (['accessToken', 'expiresAt'] as $requiredProp) {
201 if (empty($tokenData[$requiredProp])) {
202 throw new TokenException("Token file at {$tokenLocation} must contain the required property `{$requiredProp}`");
203 }
204 }
205 $expiration = \strtotime($tokenData['expiresAt']);
206 if ($expiration === \false) {
207 throw new TokenException("Cached SSO token returned an invalid expiration");
208 } elseif ($expiration < \time()) {
209 throw new TokenException("Cached SSO token returned an expired token");
210 }
211 return $tokenData;
212 }
213 /**
214 * @param array $tokenData
215 * @param string $tokenLocation
216 *
217 * @return array
218 */
219 private function writeNewTokenDataToDisk(array $tokenData, $tokenLocation) : array
220 {
221 $tokenData['expiresAt'] = \gmdate('Y-m-d\\TH:i:s\\Z', $tokenData['expiresAt']);
222 \file_put_contents($tokenLocation, \json_encode(\array_filter($tokenData)));
223 return $tokenData;
224 }
225 }
226